Auto-commit on 2024-10-25 01:44:26 by pcx3

This commit is contained in:
Stefan
2024-10-25 01:44:26 +02:00
parent 7ec1dcbe87
commit f4bb9e2e2e
149 changed files with 50757 additions and 373 deletions

View File

@@ -1,6 +1,6 @@
$TTL 1h
@ IN SOA {ns1}. {ns2}. (
2024101501 ; Serial number
2024102401 ; Serial number
1h ; Refresh interval
15m ; Retry interval
1w ; Expire interval

View File

@@ -19,12 +19,26 @@ services:
cpus: 1.0
oom_kill_disable: true
# Malware Scanner used from user panel
clamav:
image: clamav/clamav:latest
container_name: clamav
volumes:
- /home:/home
- ./clamav-db:/var/lib/clamav
restart: unless-stopped
environment:
- CLAMD_STARTUP_DELAY=30
mem_limit: 0.25g
cpus: 0.25
# OpenPanel service running on port 2083
openpanel:
image: openpanel/openpanel
container_name: openpanel
depends_on:
- openpanel_mysql
#- clamav
cap_add:
- NET_ADMIN
- SYS_MODULE
@@ -53,6 +67,8 @@ services:
- /etc/openpanel/openpanel/custom_code/custom.css:/usr/local/panel/static/css/custom.css
- /etc/openpanel/openpanel/custom_code/custom.js:/usr/local/panel/static/js/custom.js
- /etc/openpanel/openpanel/conf/knowledge_base_articles.json:/etc/openpanel/openpanel/conf/knowledge_base_articles.json
# localization
- /etc/openpanel/openpanel/translations/:/etc/openpanel/openpanel/translations/
network_mode: host
mem_limit: 1g
cpus: 1.0

View File

@@ -0,0 +1,195 @@
-- MySQL dump 10.13 Distrib 8.0.36, for Linux (x86_64)
--
-- Host: localhost Database: panel
-- ------------------------------------------------------
-- Server version 8.0.36-0ubuntu0.22.04.1
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
--
-- Database: `panel`
--
-- --------------------------------------------------------
--
-- Table structure for table `domains`
--
CREATE TABLE `domains` (
`domain_id` int NOT NULL,
`domain_name` varchar(255) NOT NULL,
`domain_url` varchar(255) NOT NULL,
`user_id` int DEFAULT NULL,
`php_version` varchar(255) DEFAULT '8.2'
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
--
-- Table structure for table `plans`
--
CREATE TABLE `plans` (
`id` int NOT NULL,
`name` varchar(255) NOT NULL,
`description` text,
`domains_limit` int NOT NULL,
`websites_limit` int NOT NULL,
`email_limit` int NOT NULL DEFAULT 0,
`ftp_limit` int NOT NULL DEFAULT 0,
`disk_limit` text NOT NULL,
`inodes_limit` bigint NOT NULL,
`db_limit` int NOT NULL,
`cpu` varchar(50) DEFAULT NULL,
`ram` varchar(50) DEFAULT NULL,
`docker_image` varchar(50) DEFAULT NULL,
`bandwidth` int DEFAULT NULL,
`storage_file` text DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
--
-- Dumping data for table `plans`
--
LOCK TABLES `plans` WRITE;
/*!40000 ALTER TABLE `plans` DISABLE KEYS */;
INSERT INTO `plans` VALUES (1,'ubuntu_nginx_mysql','Unlimited disk space and Nginx',0,10,0,0,'10 GB',1000000,0,'1','1g','openpanel/nginx',100,'0 GB'),(2,'ubuntu_apache_mysql','Unlimited disk space and Apache',0,10,0,0,'10 GB',1000000,0,'1','1g','openpanel/apache',100,'0 GB'),(3,'ubuntu_apache_mariadb','Unlimited disk space and Apache+MariaDB',0,10,0,0,'10 GB',1000000,0,'1','1g','openpanel/apache-mariadb',100,'0 GB'),(4,'ubuntu_nginx_mariadb','Unlimited disk space and Nginx+MariaDB',0,10,0,0,'10 GB',1000000,0,'1','1g','openpanel/nginx-mariadb',100,'0 GB');
/*!40000 ALTER TABLE `plans` ENABLE KEYS */;
UNLOCK TABLES;
-- --------------------------------------------------------
--
-- Table structure for table `sites`
--
CREATE TABLE `sites` (
`id` int NOT NULL,
`site_name` varchar(255) DEFAULT NULL,
`domain_id` int DEFAULT NULL,
`admin_email` varchar(255) DEFAULT NULL,
`version` varchar(20) DEFAULT NULL,
`created_date` datetime DEFAULT CURRENT_TIMESTAMP,
`type` varchar(50) DEFAULT NULL,
`ports` int DEFAULT NULL,
`path` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- --------------------------------------------------------
--
-- Table structure for table `users`
--
CREATE TABLE `users` (
`id` int NOT NULL,
`username` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
`email` varchar(255) NOT NULL,
`services` varchar(255) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT '1,2,3,4,5,6,7,8,9,10,11,12',
`user_domains` varchar(255) NOT NULL DEFAULT '',
`twofa_enabled` tinyint(1) DEFAULT '0',
`otp_secret` varchar(255) DEFAULT NULL,
`plan` varchar(255) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL,
`registered_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`server` varchar(255) DEFAULT 'default',
`plan_id` int DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
--
-- Indexes for dumped tables
--
--
-- Indexes for table `domains`
--
ALTER TABLE `domains`
ADD PRIMARY KEY (`domain_id`),
ADD KEY `user_id` (`user_id`);
--
-- Indexes for table `plans`
--
ALTER TABLE `plans`
ADD PRIMARY KEY (`id`);
--
-- Indexes for table `sites`
--
ALTER TABLE `sites`
ADD PRIMARY KEY (`id`),
ADD KEY `fk_sites_domain` (`domain_id`);
--
-- Indexes for table `users`
--
ALTER TABLE `users`
ADD PRIMARY KEY (`id`),
ADD KEY `fk_plan_id` (`plan_id`);
--
-- AUTO_INCREMENT for dumped tables
--
--
-- AUTO_INCREMENT for table `domains`
--
ALTER TABLE `domains`
MODIFY `domain_id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1;
--
-- AUTO_INCREMENT for table `plans`
--
ALTER TABLE `plans`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=5;
--
-- AUTO_INCREMENT for table `sites`
--
ALTER TABLE `sites`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1;
--
-- AUTO_INCREMENT for table `users`
--
ALTER TABLE `users`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1;
--
-- Constraints for dumped tables
--
--
-- Constraints for table `domains`
--
ALTER TABLE `domains`
ADD CONSTRAINT `domains_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`);
--
-- Constraints for table `sites`
--
ALTER TABLE `sites`
ADD CONSTRAINT `fk_sites_domain` FOREIGN KEY (`domain_id`) REFERENCES `domains` (`domain_id`);
--
-- Constraints for table `users`
--
ALTER TABLE `users`
ADD CONSTRAINT `fk_plan_id` FOREIGN KEY (`plan_id`) REFERENCES `plans` (`id`) ON DELETE SET NULL;
COMMIT;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

View File

@@ -0,0 +1,195 @@
-- MySQL dump 10.13 Distrib 8.0.36, for Linux (x86_64)
--
-- Host: localhost Database: panel
-- ------------------------------------------------------
-- Server version 8.0.36-0ubuntu0.22.04.1
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
--
-- Database: `panel`
--
-- --------------------------------------------------------
--
-- Table structure for table `domains`
--
CREATE TABLE `domains` (
`domain_id` int NOT NULL,
`domain_name` varchar(255) NOT NULL,
`domain_url` varchar(255) NOT NULL,
`user_id` int DEFAULT NULL,
`php_version` varchar(255) DEFAULT '8.2'
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
--
-- Table structure for table `plans`
--
CREATE TABLE `plans` (
`id` int NOT NULL,
`name` varchar(255) NOT NULL,
`description` text,
`domains_limit` int NOT NULL,
`websites_limit` int NOT NULL,
`email_limit` int NOT NULL DEFAULT 0,
`ftp_limit` int NOT NULL DEFAULT 0,
`disk_limit` text NOT NULL,
`inodes_limit` bigint NOT NULL,
`db_limit` int NOT NULL,
`cpu` varchar(50) DEFAULT NULL,
`ram` varchar(50) DEFAULT NULL,
`docker_image` varchar(50) DEFAULT NULL,
`bandwidth` int DEFAULT NULL,
`storage_file` text DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
--
-- Dumping data for table `plans`
--
LOCK TABLES `plans` WRITE;
/*!40000 ALTER TABLE `plans` DISABLE KEYS */;
INSERT INTO `plans` VALUES (1,'ubuntu_nginx_mysql','Unlimited disk space and Nginx',0,10,0,0,'10 GB',1000000,0,'1','1g','openpanel/nginx',100,'0 GB'),(2,'ubuntu_apache_mysql','Unlimited disk space and Apache',0,10,0,0,'10 GB',1000000,0,'1','1g','openpanel/apache',100,'0 GB');
/*!40000 ALTER TABLE `plans` ENABLE KEYS */;
UNLOCK TABLES;
-- --------------------------------------------------------
--
-- Table structure for table `sites`
--
CREATE TABLE `sites` (
`id` int NOT NULL,
`site_name` varchar(255) DEFAULT NULL,
`domain_id` int DEFAULT NULL,
`admin_email` varchar(255) DEFAULT NULL,
`version` varchar(20) DEFAULT NULL,
`created_date` datetime DEFAULT CURRENT_TIMESTAMP,
`type` varchar(50) DEFAULT NULL,
`ports` int DEFAULT NULL,
`path` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- --------------------------------------------------------
--
-- Table structure for table `users`
--
CREATE TABLE `users` (
`id` int NOT NULL,
`username` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
`email` varchar(255) NOT NULL,
`services` varchar(255) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT '1,2,3,4,5,6,7,8,9,10,11,12',
`user_domains` varchar(255) NOT NULL DEFAULT '',
`twofa_enabled` tinyint(1) DEFAULT '0',
`otp_secret` varchar(255) DEFAULT NULL,
`plan` varchar(255) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL,
`registered_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`server` varchar(255) DEFAULT 'default',
`plan_id` int DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
--
-- Indexes for dumped tables
--
--
-- Indexes for table `domains`
--
ALTER TABLE `domains`
ADD PRIMARY KEY (`domain_id`),
ADD KEY `user_id` (`user_id`);
--
-- Indexes for table `plans`
--
ALTER TABLE `plans`
ADD PRIMARY KEY (`id`);
--
-- Indexes for table `sites`
--
ALTER TABLE `sites`
ADD PRIMARY KEY (`id`),
ADD KEY `fk_sites_domain` (`domain_id`);
--
-- Indexes for table `users`
--
ALTER TABLE `users`
ADD PRIMARY KEY (`id`),
ADD KEY `fk_plan_id` (`plan_id`);
--
-- AUTO_INCREMENT for dumped tables
--
--
-- AUTO_INCREMENT for table `domains`
--
ALTER TABLE `domains`
MODIFY `domain_id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1;
--
-- AUTO_INCREMENT for table `plans`
--
ALTER TABLE `plans`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3;
--
-- AUTO_INCREMENT for table `sites`
--
ALTER TABLE `sites`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1;
--
-- AUTO_INCREMENT for table `users`
--
ALTER TABLE `users`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1;
--
-- Constraints for dumped tables
--
--
-- Constraints for table `domains`
--
ALTER TABLE `domains`
ADD CONSTRAINT `domains_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`);
--
-- Constraints for table `sites`
--
ALTER TABLE `sites`
ADD CONSTRAINT `fk_sites_domain` FOREIGN KEY (`domain_id`) REFERENCES `domains` (`domain_id`);
--
-- Constraints for table `users`
--
ALTER TABLE `users`
ADD CONSTRAINT `fk_plan_id` FOREIGN KEY (`plan_id`) REFERENCES `plans` (`id`) ON DELETE SET NULL;
COMMIT;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

View File

@@ -10,5 +10,5 @@ location /openadmin {
# roundcube
location /webmail {
return 301 https://webmail.localhost/;
return 301 http://127.0.0.1/;
}

View File

@@ -53,6 +53,12 @@
"on_dashboard": true,
"real_name": "openpanel_dns"
},
{
"name": "Malware Scanner",
"type": "docker",
"on_dashboard": false,
"real_name": "clamav"
},
{
"name": "SSL",
"type": "docker",

View File

@@ -23,9 +23,7 @@ ns3=
ns4=
email=
logout_url=
enabled_modules=dns,favorites,phpmyadmin,temporary_links,ssh,crons,backups,wordpress,pm2,disk_usage,inodes,usage,terminal,services,webserver,fix_permissions,process_manager,ip_blocker,redis,memcached,login_history,activity,twofa,domains_visitors
available_modules=malware_scan, elasticsearch
enabled_modules=dns,favorites,phpmyadmin,temporary_links,ssh,crons,backups,wordpress,flarum,pm2,disk_usage,inodes,usage,terminal,services,webserver,fix_permissions,process_manager,ip_blocker,redis,memcached,login_history,activity,twofa,domains_visitors
[USERS]
password_reset=no

View File

@@ -0,0 +1,17 @@
notify_account_login=1
notify_account_login_for_known_netblock=1
notify_account_login_notification_disabled=1
notify_autossl_expiry=0
notify_autossl_expiry_coverage=0
notify_autossl_renewal_coverage=0
notify_autossl_renewal_coverage_reduced=0
notify_autossl_renewal_uncovered_domains=0
notify_contact_address_change=1
notify_contact_address_change_notification_disabled=1
notify_disk_limit=1
notify_email_quota_limit=1
notify_password_change=1
notify_password_change_notification_disabled=1
notify_ssl_expiry=1
notify_twofactorauth_change=1
notify_twofactorauth_change_notification_disabled=1

View File

@@ -1,4 +1,4 @@
# cPanel 2 OpenPanel user import
# cPanel 2 OpenPanel account import
Free OpenPanel module to import cPanel backup in OpenPanel
Maintained by [CodeWithJuber](https://github.com/CodeWithJuber)
@@ -6,43 +6,37 @@ Maintained by [CodeWithJuber](https://github.com/CodeWithJuber)
## Features
Currently suported for import:
```
├─ DOMAINS:
│ ├─ Primary domain, Addons, Aliases and Subdomains
│ ├─ SSL certificates
│ ├─ Domains access logs (Apache domlogs)
│ └─ DNS zones
├─ WEBSITES:
│ └─ WordPress instalations from WPToolkit & Softaculous
├─ DATABASES:
│ ├─ Remote access to MySQL
│ └─ MySQL databases, users and grants
├─ PHP:
│ └─ Installed version from Cloudlinux PHP Selector
├─ FILES
├─ CRONS
├─ SSH
│ ├─ Remote SSH access
│ ├─ SSH password
│ └─ SSH keys
└─ ACCOUNT
├─ Notification preferences
├─ cPanel account creation date
└─ locale
- files and folders
- mysql databases, users and grants
- domains
- dns zones
- php version
- wp sites
- cronjobs
todo:
- ftp accounts
- email accounts
- nodejs/python apps
- postgresql
- ssh keys
- ssl certificates
Steps:
# cPanel to OpenPanel Migration Script
This script automates the process of migrating a cPanel backup to OpenPanel server. It handles various cPanel backup formats and restores essential components of the user's account.
## Features
- Supports multiple cPanel backup formats (cpmove, full backup, tar.gz, tgz, tar, zip)
- Restores user account details, domains, and hosting plan settings
- Migrates websites, databases, domains, SSL certificates, and DNS zones
- Handles PHP version settings and cron jobs
- Restores SSH access and file permissions
***emails, ftp, nodejs/python, postgres are not yet supported***
```
## Usage
1. Run the script with sudo privileges:
Run the script with sudo privileges:
```
git clone https://github.com/stefanpejcic/cPanel-to-OpenPanel
@@ -55,7 +49,8 @@ bash cPanel-to-OpenPanel/cp-import.sh --backup-location /path/to/cpanel_backup.f
## Parameters
- `--backup-location`: Path to the cPanel backup file (required)
- `--plan-name`: Name of the hosting plan in OpenPanel (required)
- `--plan-name`: Name of the hosting plan in OpenPanel (required)
- `--dry-run`: extract archive and display data without actually importing account (optional)
## Important Notes

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,35 +0,0 @@
import os
import json # will use for get to return data
import socket
from flask import Flask, Response, abort, render_template, request, send_file, g, jsonify, session, url_for, flash, redirect, get_flashed_messages
import subprocess
from app import app, is_license_valid, login_required_route
from modules.helpers import get_all_plans, is_username_unique
@app.route('/import/cpanel', methods=['GET', 'POST'])
@login_required
def import_cpanel_whm_account():
if request.method == 'POST':
path = request.form.get('path')
plan_name = request.form.get('plan_name')
if not path or not plan_name:
flash('Both path to the cPanel backup file (.tar.gz) and plan name are required!', 'error')
return redirect('/import/cpanel')
try:
file_name = os.path.basename(path)
log_file_name = f"cpanel_import_log_{os.path.splitext(file_name)[0]}"
log_file_path = f"/var/log/openpanel/admin/{log_file_name}.log"
# Run the subprocess command and redirect stdout and stderr to the log file
with open(log_file_path, 'w') as log_file:
subprocess.Popen(['opencli', 'user-import', 'cpanel', path, plan_name], stdout=log_file, stderr=log_file)
flash(f'Import started! To track the progress open the log file: {log_file_path}', 'success')
except Exception as e:
flash(f'An error occurred: {str(e)}', 'error')
return redirect('/import/cpanel')
else:
# on GET we will list the sessions in progress..
return render_template('cpanel-import.html', title='Import cPanel account')

View File

@@ -1,19 +0,0 @@
{% extends 'base.html' %}
{% block content %}
<div>
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
<div class="alert alert-warning alert-dismissible" role="alert">
{{ message }}
<a class="btn-close" data-bs-dismiss="alert" aria-label="close"></a>
</div>
{% endfor %}
{% endif %}
{% endwith %}
</div>
{% endblock %}

View File

@@ -1 +0,0 @@
@marketplace-eng

View File

@@ -1,27 +0,0 @@
# Contributing
We enthusiastically encourage contributions of all sorts to our repository, from correcting typos, to improving checks or adding new ones.
### Reporting Issues
This section guides you through submitting an issue for the Marketplace Partners. Following these guidelines helps maintainers and the community understand your report :pencil:, reproduce the behavior :computer: :computer:, and find related reports :mag_right:.
When you are reporting an issue, please [include as many details as possible](#how-do-i-submit-a-good-bug-report).
> **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one.
#### Before Submitting An Issue
* **Check the [current issues](https://github.com/digitalocean/marketplace-partners/issues)**.
#### How Do I Submit A (Good) Issue?
Issues are tracked as [GitHub issues](https://guides.github.com/features/issues/). Create an issue and provide the following information listed below.
Explain the problem and include additional details to help maintainers reproduce the problem:
* **Use a clear and descriptive title** for the issue to identify the problem.
* **Describe the exact steps which reproduce the problem** in as many details as possible. When listing steps, **don't just say what you did, but explain how you did it**.
* **Provide specific examples to demonstrate the steps**. Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets in the issue, use [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines).
* **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior.
* **Explain which behavior you expected to see instead and why.**
* **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem.

View File

@@ -1,43 +0,0 @@
# Compiled source #
###################
*.com
*.class
*.dll
*.exe
*.o
*.so
# Packages #
############
# it's better to unpack these files and commit the raw source
# git has its own built in compression methods
*.7z
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip
# Logs and databases #
######################
*.log
*.sql
*.sqlite
# OS generated files #
######################
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# App generated files #
######################
.vscode
.Rproj.user

View File

@@ -1 +0,0 @@
[admini](https://admini.vercel.app/) is the awesome default theme for OpenPanel ✌️

View File

@@ -0,0 +1,122 @@
{% extends 'base.html' %}
{% block content %}
<script type="module">
// Function to attach event listeners
function attachEventListenersForNginxEditor() {
var service_file = "{{file_path}}";
// Select the form and submit button
const form = document.querySelector('form');
const submitButton = document.querySelector('button[type="submit"]');
// Attach the click event listener to the submit button
submitButton.addEventListener('click', async (ev) => {
ev.preventDefault();
const action = submitButton.dataset.action;
const formData = new FormData(form);
const toastMessage = `Saving configuration to ` + service_file + ` and restarting service to apply changes...`;
const toast = toaster({
body: toastMessage,
className: `border-0 text-white bg-primary`,
});
try {
const response = await fetch(form.action, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams(formData).toString(),
});
// Check if the response is JSON
if (response.headers.get('content-type').includes('application/json')) {
// Parse the JSON response
const jsonResponse = await response.json();
// Display JSON response in a notification
const jsonToastMessage = JSON.stringify(jsonResponse);
const jsonToast = toaster({
body: jsonToastMessage,
className: `border-0 text-white bg-danger`,
});
} else {
const toastMessage = `{{ _("Success") }}`;
const toast = toaster({
body: toastMessage,
className: `border-0 text-white bg-success`,
});
// If the response is HTML, update the content as before
const resultHtml = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(resultHtml, 'text/html');
const mainScopeContent = doc.getElementById("main-scope")?.innerHTML;
const mainScopeElement = document.getElementById("main-scope");
if (mainScopeElement) {
mainScopeElement.innerHTML = mainScopeContent || '';
}
attachEventListenersForNginxEditor();
}
} catch (error) {
console.error('Error:', error);
}
});
}
// Attach event listeners initially
attachEventListenersForNginxEditor();
</script>
<p>{{ _('Here you can edit the main configuration file for your webserver.') }}</p>
<div id="editor-container">
<form method="post">
<textarea id="editor" name="editor_content" rows="40" cols="100">{{ file_content }}</textarea>
</div>
</section>
<script>
var editor = CodeMirror.fromTextArea(document.getElementById('editor'), {
mode: "javascript",
lineNumbers: true,
});
</script>
<footer class="main-footer btn-toolbar" role="toolbar">
<div class="btn-group" role="group" aria-label="{{ _('Status') }}">
<label>{{file_path}}</label>
</div>
<div class="ms-auto" role="group" aria-label="{{ _('Actions') }}">
<button type="submit" class="btn btn-primary">{{ _('Save Changes') }}</button></form>
</div>
</footer>
{% endblock %}

File diff suppressed because one or more lines are too long

1248
templates/admini/base.html Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,11 @@
{
"how_to_topics": [
{"title": "How to install WordPress", "link": "https://openpanel.com/docs/panel/applications/wordpress#install-wordpress"},
{"title": "Publishing a Python Application", "link": "https://openpanel.com/docs/panel/applications/pm2#python-applications"},
{"title": "How to edit Nginx / Apache configuration", "link": "https://openpanel.com/docs/panel/advanced/server_settings#nginx--apache-settings"},
{"title": "How to create a new MySQL database", "link": "https://openpanel.com/docs/panel/databases/#create-a-mysql-database"},
{"title": "How to add a Cronjob", "link": "https://openpanel.com/docs/panel/advanced/cronjobs#add-a-cronjob"},
{"title": "How to change server TimeZone", "link": "https://openpanel.com/docs/panel/advanced/server_settings#server-time"}
],
"knowledge_base_link": "https://openpanel.com/docs/panel/intro/?source=openpanel_server"
}

View File

@@ -0,0 +1,186 @@
<!-- edit_mysql_config.html -->
{% extends 'base.html' %}
{% block content %}
<script type="module">
// Function to attach event listeners
function attachEventListeners() {
// Select the form and submit button
const form = document.querySelector('form');
const submitButton = document.querySelector('button[type="submit"]');
// Attach the click event listener to the submit button
submitButton.addEventListener('click', async (ev) => {
ev.preventDefault();
const action = submitButton.dataset.action;
const formData = new FormData(form);
const toastMessage = `{{ _("Saving and restarting MySQL to apply changes...") }}`;
const toast = toaster({
body: toastMessage,
className: `border-0 text-white bg-primary`,
});
try {
const response = await fetch(form.action, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams(formData).toString(),
});
// get the response HTML content
const resultHtml = await response.text();
// Parse the HTML string to extract the content of the specific element
const parser = new DOMParser();
const doc = parser.parseFromString(resultHtml, 'text/html');
const mainScopeContent = doc.getElementById("main-scope")?.innerHTML;
// Replace the content of the element with the ID "main-scope"
const mainScopeElement = document.getElementById("main-scope");
if (mainScopeElement) {
mainScopeElement.innerHTML = mainScopeContent || '';
}
// Reattach event listeners after updating content
attachEventListeners();
} catch (error) {
console.error('Error:', error);
}
});
}
// Attach event listeners initially
attachEventListeners();
</script>
<div class="row">
<p>{{_('This tool allows you to make changes to your MySQL configuration. Modifications made here will prompt a MySQL service restart.')}} </p>
<div class="col-md-6 col-xl-8">
<div class="card card-one">
<div class="card-header">
<h6 class="card-title">{{_('MySQL Configuration Settings')}}</h6>
<nav class="nav nav-icon nav-icon-sm ms-auto">
<a href="#" onclick='location.reload(true); return false;' class="nav-link"><i class="bi bi-arrow-clockwise"></i></a>
</nav>
</div><!-- card-header -->
<div class="card-body">
<div class="gap-2 mt-3 mt-md-0">
<form method="post" action="{{ url_for('edit_mysql_config') }}">
<div class="container">
<div class="row">
{% for key in default_keys %}
<div class="col-6">
<div class="form-group">
<label for="{{ key }}" class="form-label">{{ key }}</label>
<div class="form-field">
<div class="row" id="{{ key }}">
<input type="text" class="form-control mb-2" name="{{ key }}" value="{{ current_config.get(key, '') }}">
</div>
</div>
</div></div>
{% endfor %}
</div></div>
</div>
</div><!-- card-body -->
</div>
</div>
<div class="col-md-4 col-xl-4">
<div class="card card-one">
<div class="card-header">
<h6 class="card-title">{{_('Recommended values:')}}</h6>
</div>
<div class="card-body">
<div class="row mt-2 mb-2">
<label class="card-title fw-medium mb-1">max_allowed_packet = 268435456</label>
<label class="card-title fw-medium mb-1">max_connect_errors = 100</label>
<label class="card-title fw-medium mb-1">max_connections = 100</label>
<label class="card-title fw-medium mb-1">open_files_limit = 52000</label>
<label class="card-title fw-medium mb-1">performance_schema = 0</label>
<label class="card-title fw-medium mb-1">sql_mode = ERROR_FOR_DIVISION_BY_ZERO</label>
<label class="card-title fw-medium mb-1">thread_cache_size = 256</label>
<label class="card-title fw-medium mb-1">interactive_timeout = 60</label>
<label class="card-title fw-medium mb-1">wait_timeout = 60</label>
<label class="card-title fw-medium mb-1">log_output = FILE</label>
<label class="card-title fw-medium mb-1">log_error = /var/log/mysqld.log</label>
<label class="card-title fw-medium mb-1">log_error_verbosity = 3</label>
<label class="card-title fw-medium mb-1">general_log = 0</label>
<label class="card-title fw-medium mb-1">general_log_file = /var/lib/mysql/{{current_username}}.log</label>
<label class="card-title fw-medium mb-1">long_query_time = 10</label>
<label class="card-title fw-medium mb-1">slow_query_log = 0</label>
<label class="card-title fw-medium mb-1">slow_query_log_file = /var/lib/mysql/{{current_username}}-slow.log</label>
<label class="card-title fw-medium mb-1">join_buffer_size = 1M</label>
<label class="card-title fw-medium mb-1">key_buffer_size = 71M</label>
<label class="card-title fw-medium mb-1">read_buffer_size = 131072</label>
<label class="card-title fw-medium mb-1">read_rnd_buffer_size = 262144</label>
<label class="card-title fw-medium mb-1">sort_buffer_size = 262144</label>
<label class="card-title fw-medium mb-1">innodb_log_buffer_size = 16777216</label>
<label class="card-title fw-medium mb-1">innodb_log_file_size = 16M</label>
<label class="card-title fw-medium mb-1">innodb_sort_buffer_size = 1048576</label>
<label class="card-title fw-medium mb-1">innodb_buffer_pool_chunk_size = 134217728</label>
<label class="card-title fw-medium mb-1">innodb_buffer_pool_instances = 22</label>
<label class="card-title fw-medium mb-1">innodb_buffer_pool_size = 134217728</label>
<label class="card-title fw-medium mb-1">max_heap_table_size = 1286M</label>
<label class="card-title fw-medium mb-1">tmp_table_size = 1286M</label>
</div>
</div><!-- card-body -->
</div><!-- card-one -->
</div>
</div>
</section>
<footer class="main-footer btn-toolbar" role="toolbar">
<div class="btn-group" role="group" aria-label="Status">
<label>{{ _('MySQL status:') }}</label><b> {% if mysql_status_display == 'ON' %} {{ _('Enabled') }}{% elif mysql_status_display == 'OFF' %} {{ _('Disabled') }}{% else %} {{ _('Unknown') }}{% endif %}</b>
</div>
<div class="ms-auto" role="group" aria-label="Actions">
<button type="submit" class="btn btn-primary">{{_('Save Changes')}}</button></form>
</div>
</footer>
{% endblock %}

View File

@@ -0,0 +1,743 @@
{% extends 'base.html' %}
{% block title %}Databases{% endblock %}
{% block content %}
<style>
thead {
border: 1px solid rgb(90 86 86 / 11%);
}
th {
text-transform: uppercase;
font-weight: 400;
}
</style>
<div class="row">
<!-- DatabaseWizard Modal -->
<div class="modal fade" id="databaseWizardModal" tabindex="-1" role="dialog" aria-labelledby="databaseWizardModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="databaseWizardModalLabel">{{_('Database Wizard')}}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<!-- Database Wizard Form Content -->
<form id="databaseWizardForm">
<div class="form-group">
<label for="fileName">{{_('Database Name')}}</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text" id="basic-addon3">{{current_username}}_</span>
</div>
<input type="text" name="database_name" id="dbname" class="form-control" min="1" max="{{ 63 - current_username|length }}" minlength="1" maxlength="{{ 63 - current_username|length }}" pattern="[a-zA-Z0-9_]+" title="Can only contain letters, numbers, and underscores. {{ 63 - current_username|length }} characters max." placeholder=" {{ _('Random Database Name') }}" required>
<div class="input-group-append">
<button type="button" id="generateDbName" class="btn btn-secondary rounded-0">{{_('Generate Random')}}</button>
</div>
</div></div>
<div class="form-group">
<label for="username">{{_('Username')}}</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text" id="basic-addon3">{{current_username}}_</span>
</div>
<input type="text" name="db_user" id="username" class="form-control" title="Can only contain letters, numbers, and underscores. {{ 31 - current_username|length }} characters max." min="1" max="{{ 31 - current_username|length }}" pattern="[a-zA-Z0-9_]+" minlength="1" maxlength="{{ 31 - current_username|length }}" placeholder=" {{ _('Random Username') }}" required>
<div class="input-group-append">
<button type="button" id="generateUsername" class="btn btn-secondary rounded-0">{{_('Generate Random')}}</button>
</div>
</div>
</div>
<div class="form-group">
<label for="password">{{_('Password')}}</label>
<div class="input-group">
<input type="text" name="password" id="password" class="form-control" maxlength="30" title="Can only contain letters, numbers, and underscores. 8-30 characters" min="8" max="30" minlength="8" pattern="[a-zA-Z0-9_]+" placeholder=" {{ _('Random Password') }}" required>
<div class="input-group-append">
<button type="button" id="generatePassword" class="btn btn-secondary rounded-0">{{_('Generate Random')}}</button>
</div>
</div>
</div>
</div>
<button type="submit" id="createAndAssign" class="btn btn-primary rounded-0 m-2">{{_('Create Database, User, and Grant All Privileges')}}</button>
</div>
</form>
</div>
</div>
<!-- Success Modal -->
<div class="modal fade" id="successModal" tabindex="-1" role="dialog" aria-labelledby="successModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="successModalLabel">{{_('Success!')}}</h5>
<button type="button" id="closeSuccessModal" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>{{_('Database Name:')}} {{ current_username }}_<span id="successDbName"></span></p>
<p>{{_('Username:')}} {{ current_username }}_<span id="successUsername"></span></p>
<p>{{_('Password:')}} <span id="successPassword"></span></p>
</div>
</div>
</div>
</div>
<script>
// Function to generate a random string of specified length
function generateRandomString(length) {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let randomString = "";
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * charset.length);
randomString += charset.charAt(randomIndex);
}
return randomString;
}
function displaySuccessModal() {
const dbname = document.getElementById("dbname").value;
const username = document.getElementById("username").value;
const password = document.getElementById("password").value;
refreshData();
document.getElementById("successDbName").textContent = dbname;
document.getElementById("successUsername").textContent = username;
document.getElementById("successPassword").textContent = password;
$('#successModal').modal('show'); // Show the success modal
}
function validateFormData(formData) {
// Retrieve values from the form data
const dbname = document.getElementById("dbname").value;
const username = document.getElementById("username").value;
const password = formData.get('password');
// Check if any field is missing
if (!dbname || !username || !password) {
return { valid: false, message: 'All fields are required' };
}
// Validate password
const passwordPattern = /^[a-zA-Z0-9_]+$/;
if (!passwordPattern.test(password)) {
return { valid: false, message: 'Password can only contain letters, numbers, and underscores' };
}
if (password.length < 8) { // Ensure password is at least 8 characters long
return { valid: false, message: 'Password must be at least 8 characters long' };
}
return { valid: true, message: '' };
}
// Event listener for the "Create Database, User, and Assign" button
const createAndAssignButton = document.getElementById("createAndAssign");
createAndAssignButton.addEventListener("click", function (event) {
event.preventDefault();
const buttonForWizard = document.getElementById("openDatabaseWizardButton")
buttonForWizard.disabled = true;
buttonForWizard.innerText = "Creating...";
const formElement = document.getElementById("databaseWizardForm");
const formData = new FormData(formElement);
// Hide the modal
const modal = document.getElementById("databaseWizardModal");
modal.style.display = "none";
$('.modal-backdrop').remove();
// Proceed with AJAX requests
fetch("{{ url_for('add_database') }}", { method: "POST", body: formData })
.then(() => {
return fetch("{{ url_for('add_db_user') }}", { method: "POST", body: formData });
})
.then(() => {
return fetch("{{ url_for('add_user_to_db') }}", { method: "POST", body: formData });
})
.then(() => {
// Retrieve form data for success modal
const dbname = formData.get('dbname');
const username = formData.get('username');
const password = formData.get('password');
// Show the success modal
displaySuccessModal(dbname, username, password);
buttonForWizard.disabled = false;
buttonForWizard.innerText = "Database Wizard";
})
.catch(error => {
buttonForWizard.disabled = false;
buttonForWizard.innerText = "Database Wizard";
console.error("Error:", error);
});
});
// Event listener for generating a random database name
document.getElementById("generateDbName").addEventListener("click", function () {
document.getElementById("dbname").value = generateRandomString(8);
});
// Event listener for generating a random username
document.getElementById("generateUsername").addEventListener("click", function () {
document.getElementById("username").value = generateRandomString(8);
});
// Event listener for generating a random password
document.getElementById("generatePassword").addEventListener("click", function () {
document.getElementById("password").value = generateRandomString(12);
});
// When the page loads, populate random values for the inputs
document.addEventListener("DOMContentLoaded", function () {
document.getElementById("dbname").value = generateRandomString(8);
document.getElementById("username").value = generateRandomString(8);
document.getElementById("password").value = generateRandomString(12);
});
</script>
<!-- Create Database Modal -->
<div class="modal fade" id="createDatabaseModal" tabindex="-1" role="dialog" aria-labelledby="createDatabaseModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="createDatabaseModalLabel">{{_('Create Database')}}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<!-- Create Database Form Content -->
<form method="POST" action="{{ url_for('add_database') }}">
<div class="form-group">
<label for="fileName">{{_('Database Name')}}</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text" id="basic-addon3">{{ current_username }}_</span>
</div>
<input type="text" name="database_name" class="form-control" placeholder="" aria-label="Username" aria-describedby="basic-addon1" minlength="1" min="1" max="{{ 31 - current_username|length }}" maxlength="{{ 31 - current_username|length }}" pattern="[a-zA-Z0-9_]+" title="Can only contain letters, numbers, and underscores. {{ 31 - current_username|length }} characters max." required> <button type="submit" class="btn btn-primary">{{_('Create')}}</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- ADD User Permissions Modal -->
<div class="modal fade" id="assignUserModal" tabindex="-1" aria-labelledby="assignUserModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="assignUserModalLabel">{{_('Assign user to existing MySQL database')}}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<!-- Assign User Form Content -->
<form method="POST" action="{{ url_for('add_user_to_db') }}" id="userDbpermsForm">
<div class="form">
<label for="db_user">{{_('Select user:')}}</label>
<div class="input-group">
<select name="db_user" class="form-control" required>
<option value="">{{_('Username')}}</option>
</select>
</div>
<br>
<label for="database_name">{{_('Select database:')}}</label>
<div class="input-group">
<input type="text" name="db_host" class="form-control" placeholder=" {{ _('Host') }}" value="%" hidden required>
<br>
<select name="database_name" class="form-control" required>
<option value="">{{_('Database')}}</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">{{_('Assign')}}</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- DELETE PERMS User Modal -->
<div class="modal fade" id="removeUserModal" tabindex="-1" role="dialog" aria-labelledby="removeUserModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="removeUserModalLabel">{{_('Remove user access from database')}}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<!-- Remove User Form Content -->
<form method="POST" action="{{ url_for('remove_user_from_db') }}" id="userDbForm2">
<div class="form">
<label for="db_user">{{_('Select user:')}}</label>
<div class="input-group">
<select name="db_user" class="form-control" required>
<option value="">{{_('Username')}}</option>
</select>
</div>
<br>
<label for="database_name">{{_('Select database:')}}</label>
<div class="input-group">
<input type="text" name="db_host" class="form-control" placeholder="Host" value="%" hidden required>
<br>
<select name="database_name" class="form-control" required>
<option value="">{{_('Database')}}</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">{{_('Remove Privileges')}}</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Change Password Modal -->
<div class="modal fade" id="changePasswordModal" tabindex="-1" aria-labelledby="changePasswordModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="changePasswordModalLabel">{{_('Change Password')}}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label=" {{ _('Close') }}"></button>
</div>
<div class="modal-body">
<!-- Change Password Form Content -->
<form id="changePasswordForm" method="POST" action="{{ url_for('change_mysql_user_password') }}">
<div class="form-group">
<label for="db_user">{{_('Change password for MySQL database user')}}</label>
<input type="text" name="db_user" class="form-control" placeholder=" {{ _('Database User') }}" required disabled>
<input type="text" name="db_user" class="form-control" placeholder=" {{ _('Database User') }}" required hidden>
</div>
<div class="form-group">
<label for="new_password">{{_('New Password')}}</label>
<input type="password" name="new_password" class="form-control" title="Can only contain letters, numbers, and underscores. 8-30 characters" placeholder=" {{ _('New Password') }}" min="8" max="30" minlength="8" maxlength="30" pattern="[a-zA-Z0-9_]+" required>
</div>
<input type="hidden" name="db_host" value="%">
</form>
</div>
<div class="modal-footer">
<button type="submit" form="changePasswordForm" class="btn btn-primary">{{_('Change Password')}}</button>
</div>
</div>
</div>
</div>
<!-- Create User Modal -->
<div class="modal fade" id="createUserModal" tabindex="-1" role="dialog" aria-labelledby="createUserModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="createUserModalLabel">{{_('Create new MySQL user')}}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<!-- Create User Form Content -->
<form method="POST" action="{{ url_for('add_db_user') }}">
<div class="modal-body">
<div class="form">
<label for="fileName">{{_('Username:')}}</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text" id="basic-addon3">{{ current_username }}_</span>
</div>
<input type="text" name="db_user" class="form-control" placeholder=" {{ _('Username') }}" title="Can only contain letters, numbers, and underscores. {{ 31 - current_username|length }} characters max." pattern="[a-zA-Z0-9_]+" min="1" max="{{ 31 - current_username|length }}" minlength="1" maxlength="{{ 31 - current_username|length }}" required>
</div>
<input type="text" name="db_host" class="form-control" placeholder=" {{ _('Host') }}" value="%" required hidden>
</br>
<label for="fileName">{{_('Password:')}}</label>
<div class="input-group">
<input type="password" name="password" class="form-control" min="8" max="30" maxlength="30" minlength="8" pattern="[a-zA-Z0-9_]+" placeholder=" {{ _('Password') }}" title="Can only contain letters, numbers, and underscores. 8-30 characters" required>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">{{_('Create')}}</button>
</div>
</form>
</div>
</div>
</div>
<div class="d-flex align-items-center justify-content-between mb-3">
<div><h5 class="mb-3">{{_('Databases')}} (<b id="databases-count"></b>)</h5></div>
<div class="d-flex gap-2 mt-3 mt-md-0">
<button type="button" class="btn btn-primary d-flex align-items-center gap-2" data-bs-toggle="modal" data-bs-target="#createDatabaseModal">
<i class="bi bi-plus-lg"></i> <span class="mobile-only">{{_('New DB')}}</span><span class="desktop-only">{{_('New Database')}}</span>
</button>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" id="openDatabaseWizardButton" data-bs-target="#databaseWizardModal">
<span class="mobile-only">{{_('Wizard')}}</span><span class="desktop-only">{{_('Database Wizard')}}</span>
</button>
<!--button type="button" class="btn btn-secondary d-flex align-items-center gap-2" data-toggle="modal" data-target="#repairDBModal">
<i class="bi bi-database-fill-gear"></i> Check & Repair
</button-->
</div></div>
<p class="mb-4">{{_("MySQL databases are used to store and manage your website's data, such as content, user information, and product details, making it accessible and organized for your web applications. On this page, you can easily create new databases and efficiently manage existing ones to organize and store your website's data effectively.")}}</p>
<table class="table table-hover" id="databases-table">
<thead style="position: sticky;top: 0;z-index:10;border-top:0px;" class="thead-dark">
<tr>
<th class="header">{{_('Database Name')}}</th>
<th class="header">{{_('Size')}}</th>
<th class="header">{{_('Assigned Users')}}</th>
<th class="header">{{_('Action')}}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<hr class="mt-5">
<div class="d-flex align-items-center justify-content-between mt-3 mb-3">
<div><h5 class="mb-3">{{_('Users')}} (<b id="users-count"></b>)</h5></div>
<div class="d-flex gap-2 mt-3 mt-md-0">
<button type="button" class="btn btn-primary d-flex align-items-center gap-2" data-bs-toggle="modal" data-bs-target="#createUserModal">
<i class="bi bi-plus-lg"></i> <span class="desktop-only">{{_('New')}}</span>{{_('User')}}
</button>
<button type="button" class="btn btn-secondary d-flex align-items-center gap-2" data-bs-toggle="modal" data-bs-target="#assignUserModal">{{_('Assign')}}<span class="desktop-only">{{_('to Database')}}</span>
</button>
<button type="button" class="btn btn-secondary d-flex align-items-center gap-2" data-bs-toggle="modal" data-bs-target="#removeUserModal">
{{_('Remove')}}<span class="desktop-only">{{_('from database')}}</span>
</button>
</div></div>
<style>
.header {
position: sticky;
top:0;
}
</style>
<p class="mb-4">{{_("MySQL users are essential for controlling who can access and interact with your databases, ensuring data security and controlled access to your website's information. Here you can create and manage MySQL user accounts with specific permissions, ensuring secure access to your databases.")}}</p>
<table class="table table-hover" id="users-table">
<thead style="position: sticky;top: 0;z-index:10;border-top:0px;" class="thead-dark">
<tr>
<th class="header" >{{_('User')}}</th>
<th class="header" >{{_('Action')}}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<script>
var DBrowCount = 0;
// Function to populate the databases table with sizes
function populateDatabasesTable(data) {
var DBrowCount = 0;
var databasesTable = $('#databases-table tbody');
databasesTable.empty();
// Make an AJAX request to get database sizes
$.ajax({
url: '/databases_size_info',
method: 'GET',
dataType: 'json',
success: function (sizeResponse) {
data.databases.forEach(function (database) {
var row = $('<tr>');
row.append($('<td>').text(database));
// Find the size for the current database in the response
var databaseSizeInfo = sizeResponse.find(function (item) {
return item.Database === database;
});
// Display the size if found, otherwise display N/A
var databaseSizeHere = databaseSizeInfo ? formatBytes(databaseSizeInfo['Size (BYTES)']) : 'N/A';
row.append($('<td>').text(databaseSizeHere));
var assignedUsers = data.assigned_databases.find(function (item) {
return item.database === database;
});
row.append($('<td>').text(assignedUsers ? assignedUsers.users : ''));
// Create a form with the delete button
var formHtml =
'<div class="d-flex gap-2 mt-3 mt-md-0">' +
{% if 'phpmyadmin' in enabled_modules %}
'<a href="/phpmyadmin?route=/database/structure&server=1&db=' + database + '" target="_blank" class="btn btn-primary" type="button"> phpMyAdmin <i class="bi bi-box-arrow-up-right"></i></a>' +
{% endif %}
'<form method="POST" action="{{ url_for("delete_database") }}">' +
'<input type="hidden" name="database_name" value="' + database + '">' +
'<button class="btn btn-danger" type="button" onclick="confirmDelete(this);"><i class="bi bi-trash3"></i> Delete</button>' +
'</form>' +
'</div>';
row.append($('<td>').html(formHtml));
databasesTable.append(row);
DBrowCount++;
});
$('#databases-count').text(DBrowCount);
},
error: function (error) {
console.error('Error fetching database sizes:', error);
}
});
}
// Helper function to format bytes into a human-readable format
function formatBytes(bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
function confirmDelete(button) {
var countdown = 5;
var countdownActive = true; // Variable to track countdown status
// Change the button style and text
$(button).removeClass('btn-danger').addClass('btn-dark').html('<i class="bi bi-trash3-fill"></i> Confirm <span class="btn-indicator btn-indicator-mini bg-danger">' + countdown + '</span>');
// Interval to update countdown
var intervalId = setInterval(function () {
countdown--;
// Update the countdown value in the button text
$(button).find('.btn-indicator-mini').text(countdown);
// Remove the onclick event to prevent further changes on subsequent clicks
$(button).removeAttr('onclick');
// If countdown reaches 0, revert the button, clear the interval, and set countdownActive to false
if (countdown === 0) {
clearInterval(intervalId);
revertButton(button);
countdownActive = false;
}
}, 1000);
// Add a click event to the confirm button
$(button).on('click', function () {
// Check if countdown is active before allowing form submission
if (countdownActive) {
// Submit the parent form when the button is clicked during the countdown
$(button).closest('form').submit();
}
});
}
// Function to revert the button to its initial state
function revertButton(button) {
$(button).removeClass('btn-dark').addClass('btn-danger').html('<i class="bi bi-trash3"></i> Delete');
$(button).attr('onclick', 'confirmDelete(this);');
}
var UsersrowCount = 0;
// Function to populate the users table
function populateUsersTable(data) {
var usersTable = $('#users-table tbody');
usersTable.empty();
UsersrowCount = 0;
data.users.forEach(function (user) {
var row = $('<tr>');
row.append($('<td id="databaseUsername">').text(user));
// Create a form with the delete button
var formHtml =
'<div class="d-flex gap-2 mt-3 mt-md-0">' +
'<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#changePasswordModal"><i class="bi bi-key-fill"></i> Change Password</button>' +
'<form method="POST" action="{{ url_for("delete_db_user") }}">' +
'<input type="hidden" name="db_user" value="' + user + '">' +
'<button class="btn btn-danger" type="button" onclick="confirmUserDelete(this);"><i class="bi bi-trash3"></i> Delete</button>' +
'</form>' +
'</div>';
row.append($('<td>').html(formHtml));
usersTable.append(row);
UsersrowCount++;
});
$('#users-count').text(UsersrowCount);
}
// Function to handle the confirm delete action for users
function confirmUserDelete(button) {
// Change the button style and text
$(button).removeClass('btn-danger').addClass('btn-warning').html('<i class="bi bi-trash3-fill"></i> Confirm');
// Remove the onclick event to prevent further changes on subsequent clicks
$(button).removeAttr('onclick');
// Add a new click event to the confirm button
$(button).on('click', function () {
// Submit the parent form when the button is clicked again
$(button).closest('form').submit();
});
}
function refreshData() {
$.ajax({
url: '/databases_info',
dataType: 'json',
success: function (data) {
populateDatabasesTable(data);
populateUsersTable(data);
// Populate the select dropdowns with data
var userSelect = $("select[name='db_user']");
var databaseSelect = $("select[name='database_name']");
// Populate the User select dropdown
$.each(data.users, function(index, user) {
userSelect.append($('<option>', {
value: user,
text: user
}));
});
// Populate the Database select dropdown
$.each(data.databases, function(index, database) {
databaseSelect.append($('<option>', {
value: database,
text: database
}));
});
},
error: function (error) {
console.error('Error fetching data:', error);
}
});
}
refreshData();
function changePassModal() {
// Handle the modal show event
$('#changePasswordModal').on('show.bs.modal', function (event) {
// Get the button that triggered the modal
var button = $(event.relatedTarget);
// Find the closest <tr> element to the button
var row = button.closest('tr');
// Find the <td> element with the id "databaseUsername" and get its text
var databaseUsername = row.find('#databaseUsername').text();
// Set the value of the "Database User" field in the modal
$('#changePasswordForm [name="db_user"]').val(databaseUsername);
});
};
changePassModal();
// Add an event listener to the "x" button to close the Success Modal
document.getElementById("closeSuccessModal").addEventListener("click", function () {
// Close the Success Modal
$('#successModal').modal('hide');
});
// Event listener for opening the Database Wizard Modal
document.getElementById("openDatabaseWizardButton").addEventListener("click", function () {
// Regenerate random values for the inputs
document.getElementById("dbname").value = generateRandomString(8);
document.getElementById("username").value = generateRandomString(8);
document.getElementById("password").value = generateRandomString(12);
});
// OPEN WIZARD MODAL ON URL HASH
function openWizardModalOnURL() {
const currentFragment = window.location.hash;
const addNewFragment = "#wizard";
if (currentFragment === addNewFragment) {
const modalElement = document.getElementById('databaseWizardModal');
const modal = new bootstrap.Modal(modalElement);
modal.show();
}
}
// Listen for changes in the URL's fragment identifier
window.addEventListener('hashchange', openWizardModalOnURL);
// Check the initial fragment identifier when the page loads
window.addEventListener('load', openWizardModalOnURL);
</script>
{% endblock %}

View File

@@ -0,0 +1,37 @@
{% extends 'base.html' %}
{% block content %}
<div class="row">
<p>{{ _('This interface lists all of the processes that currently run on any database on your server.') }}</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th data-toggle="tooltip" data-placement="bottom" title="{{ _('Process ID: a unique identification number assigned by Linux systems to each process and application.') }}">Id</th>
<th data-toggle="tooltip" data-placement="bottom" title="{{ _('Indicates the MySQL user who executed the process on the database.') }}">{{ _('User') }}</th>
<th data-toggle="tooltip" data-placement="bottom" title="{{ _('Displays the client&apos;s hostname and the port from which the process was executed (e.g., Host:0000).') }}">{{ _('Host') }}</th>
<th data-toggle="tooltip" data-placement="bottom" title="{{ _('The database on which the process is running. This column will display NULL if the process is not associated with any database.') }}">DB</th>
<th data-toggle="tooltip" data-placement="bottom" title="{{ _('Type of command issued by the system to the database.') }}">{{ _('Command') }}</th>
<th data-toggle="tooltip" data-placement="bottom" title="{{ _('The duration, in seconds, that the process has remained in its current state.') }}">{{ _('Time') }}</th>
<th data-toggle="tooltip" data-placement="bottom" title="{{ _('The action, state, or event of the process. This column will display NULL for processes in the SHOW PROCESSLIST state.') }}">{{ _('State') }}</th>
<th data-toggle="tooltip" data-placement="bottom" title="{{ _('Text of the statement that the process is currently executing. If the process is not executing a statement, this column displays NULL. It may also show a statement sent to the server or an inner statement used to execute other statements.') }}">{{ _('Info') }}</th>
</tr>
</thead>
<tbody>
{% for row in processlist_output.split('\n')[2:] %}
{% if row %}
<tr>
{% for column in row.split('\t') %}
<td>{{ column }}</td>
{% endfor %}
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View File

@@ -0,0 +1,191 @@
<!-- remotemysql.html -->
{% extends 'base.html' %}
{% block content %}
<script type="module">
// Function to attach event listeners
function attachEventListeners() {
document.querySelectorAll("button[type='submit']").forEach((btn) => {
btn.addEventListener("click", async (ev) => {
ev.preventDefault();
const action = btn.closest("form").querySelector("input[name='action']").value;
let btnClass, toastMessage, enabledMessage;
if (action === 'enable') {
btnClass = 'danger';
toastMessage = "{{ _('Enabling remote MySQL access..') }}";
enabledMessage = "{{ _('Remote MySQL access is enabled.') }}";
} else if (action === 'disable') {
btnClass = 'success';
toastMessage = "{{ _('Disabling remote MySQL access..') }}";
enabledMessage = "{{ _('Remote MySQL access is disabled.') }}";
}
const toast = toaster({
body: toastMessage,
className: `border-0 text-white bg-${btnClass}`,
});
try {
const response = await fetch(window.location.href, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `action=${action}`,
});
// Check if the user is still on the same URL
if (window.location.pathname === '/databases/remote-mysql') {
// get the response HTML content
const resultHtml = await response.text();
// Parse the HTML string to extract the content of the specific element
const parser = new DOMParser();
const doc = parser.parseFromString(resultHtml, 'text/html');
const mainScopeContent = doc.getElementById("main-scope")?.innerHTML;
// Replace the content of the element with the ID "main-scope"
const mainScopeElement = document.getElementById("main-scope");
if (mainScopeElement) {
mainScopeElement.innerHTML = mainScopeContent || '';
}
// Reattach event listeners after updating content
attachEventListeners();
} else {
const toast = toaster({
body: enabledMessage,
className: `border-0 text-white bg-primary`,
});
}
} catch (error) {
console.error('Error:', error);
}
});
});
}
// Attach event listeners initially
attachEventListeners();
</script>
<div class="row">
<p>{{ _('Remote MySQL access gives you the ability to connect to a MySQL database on this server from a another (remote) device or location over the internet.') }}</p>
<div class="col-xl-12">
<div class="card card-one">
<div class="card-body">
<div class="row mt-2 mb-2">
<label class="card-title fw-medium text-dark mb-1">{{ _('status:') }}</label><div class="col-6">
{% if remote_mysql_display == 'ON' %}
<h3 class="card-value mb-1"><i class="bi bi-check-circle-fill"></i> {{ _('Enabled<') }}/h3>
{% elif remote_mysql_display == 'OFF' %}
<h3 class="card-value mb-1"><i class="bi bi-x-lg"></i> {{ _('Disabled') }}</h3>
{% else %}
<h3 class="card-value mb-1"><i class="bi bi-x-lg"></i>{{ _(' Unknown. Contact Administrator.') }}</h3>
{% endif %}
</div><!-- col -->
</div>
{% if remote_mysql_display == 'ON' %}
<hr>
<div class="row mt-2 mb-2">
<div class="col-6">
<label class="card-title fw-medium text-dark mb-1">{{ _('Server IP address:') }}</label><h3 class="card-value mb-1">{{server_ip}}</h3>
</div><!-- col -->
</div>
<hr>
<div class="row mt-2 mb-2">
<div class="col-12">
<label class="card-title fw-medium text-dark mb-1">{{ _('MySQL Port:') }}</label>
<h3 class="card-value mb-1">{{container_port}}</h3><span class="d-block text-muted fs-11 ff-secondary lh-4">{{ _('*Port is random generated and unique to your account.') }}</span>
</div><!-- col -->
</div><!-- row -->
{% elif remote_mysql_display == 'OFF' %}
{% else %}
{% endif %}
</div><!-- card-body -->
</div><!-- card-one -->
</div>
{% if remote_mysql_display == 'OFF' %}
<div class="col-xl-12">
<div class="card card-one">
<div class="card-header">
<h6 class="card-title">{{ _('Important Security Notice') }}</h6>
<nav class="nav nav-icon nav-icon-sm ms-auto">
<a href="" class="nav-link"><i class="ri-refresh-line"></i></a>
<a href="" class="nav-link"><i class="ri-more-2-fill"></i></a>
</nav>
</div><!-- card-header -->
<div class="card-body">
<div class="gap-2 mt-3 mt-md-0">
<p class="mb-3">{{ _('Allowing remote MySQL access opens your database to connections from the entire internet, which may pose a security risk. Please consider the following:') }}</p>
<ul>
<li><b>{{ _('Security Vulnerabilities') }}</b>: {{ _('Allowing access from the web can expose your database to potential security vulnerabilities, increasing the risk of unauthorized access, data breaches, and data loss.') }}</li>
<li><b>{{ _('Data Privacy') }}</b>: {{ _('Your sensitive data may be at risk if not properly secured. Make sure to use strong passwords and encryption to protect your information.') }}
<li><b>{{ _('Firewall and Access Control') }}</b>: {{ _('It is crucial to set up robust firewall rules and access control to restrict connections only to trusted IP addresses.') }}
<li><b>{{ _('Regular Backups') }}</b>: {{ _('Ensure that you have regular database backups in place to recover data in case of any security incidents.') }}
</ul>
<p class="mb-3">{{ _("Before enabling remote MySQL access, please review your security settings, and consider the potential risks carefully. If you're unsure about the security implications or need assistance, consult with your system administrator or a security expert.") }}</p>
<p class="mb-3">{{ _('Your data security is important to us, and we recommend taking the necessary precautions to protect it.') }}</p>
</div>
</div>
</div>
</div><!-- card-body -->
</div>
{% endif %}
</section>
<footer class="main-footer btn-toolbar" role="toolbar">
<div class="btn-group" role="group" aria-label="Status">
<label>status:</label><b> {% if remote_mysql_display == 'ON' %} {{ _('Enabled') }}{% elif remote_mysql_display == 'OFF' %} {{ _('Disabled') }}{% else %} {{ _('Unknown') }}{% endif %}</b>
</div>
<div class="ms-auto" role="group" aria-label="Actions">
{% if remote_mysql_display == 'ON' %}
<form method="post">
<input type="hidden" name="action" value="disable">
<button class="btn btn-success d-flex align-items-center gap-2" type="submit">{{ _('Disable Remote MySQL Access') }}</button>
</form>
{% elif remote_mysql_display == 'OFF' %}
<form method="post">
<input type="hidden" name="action" value="enable">
<button class="btn btn-danger d-flex align-items-center gap-2" type="submit">{{ _('Enable Remote MySQL Access') }}</button>
</form>
{% else %}
<form method="post">
<input type="hidden" name="action" value="disable">
<button class="btn btn-primary d-flex align-items-center gap-2" type="submit">{{ _('Disable Remote MySQL Access') }}</button>
</form>
<form method="post">
<input type="hidden" name="action" value="enable">
<button class="btn btn-primary d-flex align-items-center gap-2" type="submit">{{ _('Enable Remote MySQL Access') }}</button>
</form>
{% endif %}
</div>
</footer>
{% endblock %}

View File

@@ -0,0 +1,892 @@
{% extends 'base.html' %}
{% block content %}
<style>
td {vertical-align: middle;}
@media (min-width: 768px) {
table.table {
table-layout: fixed;
}
table.table td {
word-wrap: break-word;
}
}
@media (max-width: 767px) {
span.advanced-text {display:none;}
}
.hidden {
display: none;
}
.advanced-settings {
align-items: center;
text-align: right;
color: black;
}
.advanced-settings i {
margin-right: 5px;
transform: rotate(-90deg);
transition: transform 0.3s ease-in-out;
}
[data-bs-theme=light] .domain_link {
color: black;
}
.domain_link {
border-bottom: 1px dashed #999;
text-decoration: none;
}
.advanced-settings.active i {
transform: rotate(0);
}
thead {
border: 1px solid rgb(90 86 86 / 11%);
}
th {
text-transform: uppercase;
font-weight: 400;
}
</style>
<div class="row">
<div id="toAddActive" class="CrawlerStatusCard">
<!-- "Add New Domain" form -->
<form id="addDomainForm" method="POST" action="{{ url_for('add_domain') }}" style="display: none;">
<div class="col-md-12">
<div class="card mb-3" style="">
<div class="card-body">
<div class="row">
<div class="col-6">
<label for="domain_url">{{ _('Domain Name:') }}</label>
<div class="form-group">
<input type="text" class="form-control" name="domain_url" id="domain_url" placeholder="example.net" required>
<input type="text" class="form-control d-none" name="domain_name" id="domain_name" placeholder="example.net" hidden >
</div>
<small id="punycode-info" style="display: none;"></small>
</div>
<div class="col-6">
<label for="domain_name">{{ _('Document Root (folder):') }}</label>
<div class="form-group">
<input type="text" class="form-control" name="doc_root" id="doc_root" value="/home/{{current_username}}/" disabled>
</div>
<small>{{ _('*Document root for now can not be changed, but this feature will soon be added.') }}</small>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/punycode/2.1.1/punycode.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
var punycodeElements = document.querySelectorAll(".punycode");
punycodeElements.forEach(function(element) {
element.textContent = punycode.toUnicode(element.textContent);
});
});
</script>
<hr class="my-4">
<p>{{ _('When you create a domain or subdomain, the system will attempt to secure that domain with a free') }} <i>Let's Encrypt</i> {{ _('certificate.') }}</p>
<p> <span class="lead">
<button type="submit" id="installButton" class="btn btn-lg btn-primary">{{ _('Add Domain') }}</button></span> <span><input type="checkbox" id="preventRedirectCheckbox"><span data-toggle="tooltip" data-placement="bottom" title="{{ _('After adding the domain stay on this page to add another domain.') }}">&nbsp; {{ _('Stay on this page') }}</span></input></span>
</p>
</div>
</div>
</div>
</form>
</div>
<div id="statusMessage" class="d-none"></div>
<style>
@keyframes rotate {
to {
--angle: 360deg
}
}
@property --angle {
syntax: "<angle>";
initial-value: 0deg;
inherits: false
}
.CrawlerStatusCard.active {
animation: rotate 2s linear infinite;
background: hsla(0,0%,100%,.5);
border: 2px solid transparent;
border-image: conic-gradient(from var(--angle),transparent 0deg 90deg,transparent 90deg 180deg,transparent 180deg 270deg,#0077bc 270deg 1turn) 1 stretch;
}
</style>
{% if total_pages > 1 %}
<div class="container">
<div class="row">
<div class="col-sm">
<p>
{{ _('Showing domains') }} {{ start_line_number }} {{ _('to') }} {{ end_line_number }}, {{ _('from a total of') }} {{ total_domains }}.
</p>
</div>
<div class="col-sm">
<nav aria-label="{{ _('Page navigation') }}">
<ul class="pagination">
{% if current_page > 1 %}
<li class="page-item">
<a class="page-link" href="{{ current_route }}?page={{ current_page-1 }}" aria-label="{{ _('Previous') }}">
<span aria-hidden="true">&laquo;{{ _(' Previous') }}</span>
</a>
</li>
{% endif %}
{% for p in range(1, total_pages+1) %}
{% if p == current_page %}
<li class="page-item active">
<span class="page-link">{{ p }}</span>
</li>
{% elif p == 1 or p == total_pages or (p >= current_page - 2 and p <= current_page + 2) %}
<li class="page-item">
<a class="page-link" href="{{ current_route }}?page={{ p }}">{{ p }}</a>
</li>
{% elif p == 2 or p == total_pages - 1 %}
<li class="page-item disabled">
<span class="page-link">...</span>
</li>
{% endif %}
{% endfor %}
{% if current_page < total_pages %}
<li class="page-item">
<a class="page-link" href="{{ current_route }}?page={{ current_page+1 }}" aria-label="{{ _('Next') }}">
<span aria-hidden="true">{{ _('Next') }} &raquo;</span>
</a>
</li>
{% endif %}
</ul>
</nav>
</div>
</div>
</div>
<br>
{% endif %}
<table id="allDomainsTable" class="table table-hover">
<thead>
<tr>
<th>{{ _('Domain Name') }}</th>
<th data-toggle="tooltip" data-placement="bottom" title="{{ _('The document root refers to the directory where the content of a domain name is displayed from. It serves as the designated folder where website files should be stored.') }}" class="desktop-only">{{ _('Document Root') }}</th>
<!--th>Note *(optional)</th-->
<th data-toggle="tooltip" data-placement="bottom" title="{{ _('Within the DNS zone, you have the capability to create or oversee various records, including but not limited to A, AAAA, MX, TXT, and more.') }}">{{ _('DNS') }}<span class="desktop-only">{{ _(' Zone') }}</span></th>
<th data-toggle="tooltip" data-placement="bottom" title="{{ _('Here, you have the option to redirect traffic to a different URL. Once activated, all query parameters will be transmitted to the specified url.') }}">{{ _('Redirect') }}</th>
<th style="width: 10%;" data-toggle="tooltip" data-placement="bottom" title="{{ _('When a domain possesses SSL, Force HTTPS is activated automatically, redirecting all traffic to utilize the secure https protocol. However, if a domain lacks an SSL, Force HTTPS cannot be enabled.') }}"><span class="desktop-only">{{ _('Force ') }}</span>{{ _('HTTPS') }}</th>
<th style="text-align: right; padding-right: 12px; width: 5%;"><i class="bi bi-three-dots"></i></th>
</tr>
</thead>
<tbody>
{% for domain in domains %}
<tr class="domain_row">
<td>
<a class="domain_link" href="http://{{ domain.domain_url }}" target="_blank">
<span class="punycode">{{ domain.domain_url }}</span> <i class="bi bi-box-arrow-up-right"></i>
</a>
<small class="mobile-only">
<br>
<a href="/files/{{ domain.domain_url }}"><i style="color: orange;" class="bi bi-folder-fill"></i> <span class="domain_link">/home/{{ current_username }}/{{ domain.domain_url }}</span></a>
</small>
</td>
<td class="desktop-only"><a href="/files/{{ domain.domain_url }}"><i style="color: orange;" class="bi bi-folder-fill"></i> <span class="domain_link">/home/{{ current_username }}/{{ domain.domain_url }}</span></a></td>
<!--td>{{ domain.domain_name }}</td-->
<td>
<a href="/domains/edit-dns-zone/{{ domain.domain_url }}" type="button" class="btn btn-transparent"><span class="mobile-only"><i class="bi bi-pencil-fill"></i></span><span class="desktop-only">{{ _('Edit DNS') }}</span></a>
</td>
<td>
{% if domain.redirect_url %}
<div id="edit-form-{{ domain.domain_id }}" style="display: none;">
<form action="/set_redirect" method="post">
<input type="hidden" name="domain_id" value="{{ domain.domain_id }}">
<input type="hidden" name="domain_name" value="{{ domain.domain_name }}">
<input type="hidden" name="domain_url" value="{{ domain.domain_url }}">
<div class="form-group">
<input type="text" class="form-control" name="redirect_url" value="{{ domain.redirect_url }}">
</div>
<button type="submit" class="btn btn-primary"><span class="mobile-only"><i class="bi bi-check-lg" style="color:green"></i></span><span class="desktop-only">{{ _('Save') }}</span></button>
<button type="button" class="btn btn-secondary" onclick="cancelEdit('{{ domain.domain_id }}')"><span class="mobile-only"><i class="bi bi-x-lg" style="color:red"></i></span><span class="desktop-only">{{ _('Cancel') }}</span></button>
</form>
</div>
<div id="edit-buttons-{{ domain.domain_id }}" style="display: inline-flex;">
<span id="redirect-value-{{ domain.domain_id }}">{{ domain.redirect_url }}</span>
<button type="button" class="btn btn-sm btn-transparent" onclick="editRedirect('{{ domain.domain_id }}')"><i class="bi bi-pencil"></i></button>
<form action="/delete_redirect" method="post">
<input type="hidden" name="domain_id" value="{{ domain.domain_id }}">
<input type="hidden" name="redirect_url" value="{{ domain.redirect_url }}">
<button type="submit" class="btn btn-sm btn-transparent" style="color:red;"><i class="bi bi-x-lg"></i></button>
</form>
</div>
{% else %}
<div id="create-form-{{ domain.domain_id }}" style="display: none;">
<form action="/set_redirect" method="post">
<input type="hidden" name="domain_id" value="{{ domain.domain_id }}">
<input type="hidden" name="domain_name" value="{{ domain.domain_name }}">
<input type="hidden" name="domain_url" value="{{ domain.domain_url }}">
<div class="form-group">
<input type="text" class="form-control" name="redirect_url" placeholder="{{ _('Enter redirect URL') }}">
</div>
<button type="submit" class="btn btn-transparent"><span class="mobile-only"><i class="bi bi-check-lg" style="color:green"></i></span><span class="desktop-only">{{ _('Save') }}</span></button>
<button type="button" class="btn btn-transparent" onclick="cancelCreate('{{ domain.domain_id }}')"><span class="mobile-only"><i class="bi bi-x-lg" style="color:red"></i></span><span class="desktop-only">{{ _('Cancel') }}</span></button>
</form>
</div>
<button type="button" class="btn btn-transparent" onclick="showRedirectInput('{{ domain.domain_id }}')" id="create-button-{{ domain.domain_id }}"><span class="mobile-only"><i class="bi bi-plus-circle-fill"></i></span><span class="desktop-only">{{ _('Create Redirect') }}</span></button>
{% endif %}
{% if domain.https != "Unknown" %}
<td>
<input type="hidden" name="domain" value="{{ domain.domain_url }}">
<div class="form-check form-switch">
<input class="custom-control-input form-check-input" type="checkbox" role="switch" id="domainSwitch{{ loop.index }}" {% if domain.https|string == "True" %} checked {% endif %}>
<label class="custom-control-label" for="domainSwitch{{ loop.index }}"></label>
</div>
</td>
{% else %}
<td></td>
{% endif %}
<td style="text-align: right;">
<div class="dropdown">
<a class="dropdown-link advanced-settings" type="button" id="delete-menu" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="bi bi-three-dots"></i>
</a>
<div class="dropdown-menu dropdown-menu-end" aria-labelledby="{{ _('delete-menu') }}">
<form action="/domains/edit-vhosts" method="get">
<input type="hidden" name="domain" value="{{ domain.domain_url }}">
<button type="submit" id="virtualhosts" class="btn btn-warning dropdown-item"><i class="bi bi-file-earmark-binary"></i>{{ _(' Edit VirtualHosts') }}</button>
</form>
<form action="/delete_domain" method="post">
<input type="hidden" name="domain_id" value="{{ domain.domain_id }}">
<button type="button" class="btn btn-danger dropdown-item" data-bs-toggle="modal" data-bs-target="#deleteConfirmationModal" onclick="setDeleteDomain('{{ domain.domain_id }}', '{{ domain.domain_name }}')">
<i class="bi bi-trash3"></i>{{ _(' Delete Domain') }}</button>
</form>
</div>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if total_pages > 1 %}
<div class="container">
<div class="row">
<div class="col-sm">
<p>
{{ _('Showing domains') }} {{ start_line_number }} {{ _('to') }} {{ end_line_number }}, {{ _('from a total of') }} {{ total_domains }}.
</p>
</div>
<div class="col-sm">
<nav aria-label="{{ _('Page navigation') }}">
<ul class="pagination">
{% if current_page > 1 %}
<li class="page-item">
<a class="page-link" href="{{ current_route }}?page={{ current_page-1 }}" aria-label="{{ _('Previous') }}">
<span aria-hidden="true">&laquo; {{ _('Previous') }}</span>
</a>
</li>
{% endif %}
{% for p in range(1, total_pages+1) %}
{% if p == current_page %}
<li class="page-item active">
<span class="page-link">{{ p }}</span>
</li>
{% elif p == 1 or p == total_pages or (p >= current_page - 2 and p <= current_page + 2) %}
<li class="page-item">
<a class="page-link" href="{{ current_route }}?page={{ p }}">{{ p }}</a>
</li>
{% elif p == 2 or p == total_pages - 1 %}
<li class="page-item disabled">
<span class="page-link">...</span>
</li>
{% endif %}
{% endfor %}
{% if current_page < total_pages %}
<li class="page-item">
<a class="page-link" href="{{ current_route }}?page={{ current_page+1 }}" aria-label="{{ _('Next') }}">
<span aria-hidden="true">{{ _('Next') }} &raquo;</span>
</a>
</li>
{% endif %}
</ul>
</nav>
</div>
</div>
</div>
{% endif %}
</div>
<script>
async function HttpsToggleSelector () {
$('.custom-control-input').change(function () {
const isChecked = $(this).prop('checked');
const domainUrl = $(this).closest('td').find('input[name="domain"]').val();
$.ajax({
type: 'POST',
url: '/domains/enable-https',
data: {
domain_url: domainUrl,
https_enabled: isChecked
},
success: function (response) {
// Display the response as a toast notification for the specific toast
$('#https-toast #responseMessage').text(response.message);
$('#https-toast #responseToast').toast('show');
},
error: function (xhr, status, error) {
// Handle error response if needed
console.error(xhr.responseText);
// Display an error message as a toast notification for the specific toast
$('#https-toast #responseMessage').text('Error: ' + xhr.responseText);
$('#https-toast #responseToast').toast('show');
}
});
});
};
HttpsToggleSelector ();
</script>
<script>
function showRedirectInput(domainId) {
document.getElementById('create-button-' + domainId).style.display = 'none';
document.getElementById('create-form-' + domainId).style.display = 'block';
}
function cancelCreate(domainId) {
document.getElementById('create-form-' + domainId).style.display = 'none';
document.getElementById('create-button-' + domainId).style.display = 'inline';
}
function editRedirect(domainId) {
var editButtons = document.getElementById('edit-buttons-' + domainId);
if (editButtons) editButtons.style.display = 'none';
var cancelButton = document.querySelector('#edit-buttons-' + domainId + ' button:last-child');
if (cancelButton) cancelButton.style.display = 'inline-flex';
var editForm = document.getElementById('edit-form-' + domainId);
if (editForm) editForm.style.display = 'block';
}
function cancelEdit(domainId) {
var editButtons = document.getElementById('edit-buttons-' + domainId);
if (editButtons) editButtons.style.display = 'inline-flex';
var cancelButton = document.querySelector('#edit-buttons-' + domainId + ' button:last-child');
if (cancelButton) cancelButton.style.display = 'inline-flex';
var editForm = document.getElementById('edit-form-' + domainId);
if (editForm) editForm.style.display = 'none';
}
</script>
<!-- Confirmation Modal -->
<div class="modal fade" id="deleteConfirmationModal" tabindex="-1" role="dialog" aria-labelledby="deleteConfirmationModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteConfirmationModalLabel">{{ _('Confirm Domain Deletion') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ _('Close') }}"></button>
</div>
<div class="modal-body">
<span class="error_message"></span>
<div class="confirm_message">{{ _('Are you sure you want to delete the domain?') }}</div>
<div class="website-list"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ _('Cancel') }}</button>
<form action="/delete_domain" method="post">
<input type="hidden" id="deleteDomainId" name="domain_id" value="">
<button type="submit" class="btn btn-danger" id="deleteButton">{{ _('Delete') }}</button>
</form>
</div>
</div>
</div>
</div>
<script>
function setDeleteDomainId(domainId) {
document.getElementById('deleteDomainId').value = domainId;
}
</script>
</section>
<footer class="main-footer btn-toolbar" role="toolbar">
<div class="btn-group" role="group" aria-label="{{ _('Status') }}">
<input type="text" class="form-control" placeholder="{{ _('Search domains') }}" id="searchDomainInput">
</div>
<div class="ms-auto" role="group" aria-label="{{ _('Actions') }}">
<button type="button" class="btn btn-primary d-flex align-items-center gap-2" id="showAddDomainFormBtn"><i class="bi bi-plus-lg"></i> <span class="desktop-only">{{ _('Add Domain') }}</span></button>
</div>
</footer>
<script>
function initializeDomainManagement() {
// Get references to search input and display settings div
const searchDomainInput = document.getElementById("searchDomainInput");
// Get all domain rows to be used for filtering
const domainRows = document.querySelectorAll(".domain_row");
// Handle search input changes
searchDomainInput.addEventListener("input", function() {
const searchTerm = searchDomainInput.value.trim().toLowerCase();
// Loop through domain rows and hide/show based on search term
domainRows.forEach(function(row) {
const domainName = row.querySelector("td:nth-child(1)").textContent.toLowerCase();
const domainUrl = row.querySelector("td:nth-child(2)").textContent.toLowerCase();
// Show row if search term matches domain name or domain URL, otherwise hide it
if (domainName.includes(searchTerm) || domainUrl.includes(searchTerm)) {
row.style.display = "table-row";
} else {
row.style.display = "none";
}
});
});
}
initializeDomainManagement ();
</script>
<script>
const addDomainForm = document.getElementById("addDomainForm");
const domainNameInput = document.querySelector('input[name="domain_name"]');
const domainUrlInput = document.querySelector('input[name="domain_url"]');
const allDomainsTable = document.getElementById("allDomainsTable");
// Handle form submission
addDomainForm.addEventListener("submit", function(event) {
event.preventDefault();
allDomainsTable.style.display = "none"; // Hide the form
if (domainNameInput.value.trim() === '') {
domainNameInput.value = extractDomainName(domainUrlInput.value);
}
// Use the Punycode value in form submission if available
if (punycodeValueForSubmission !== undefined) {
domainUrlInput.value = punycodeValueForSubmission;
}
let btnClass, toastMessage;
const installButton = event.target.querySelector("#installButton");
// Check if the domain URL contains a valid domain name
const validDomain = isValidDomainName(domainUrlInput.value);
if (!validDomain) {
btnClass = 'danger';
toastMessage = "{{ _('Invalid domain name. Please enter a valid domain name like') }} 'example.com' {{ _('or') }} 'sub.example.com'.";
const toast = toaster({
body: toastMessage,
className: `border-0 text-white bg-${btnClass}`,
});
event.preventDefault();
} else {
// Use the Punycode value in form submission if available
if (punycodeValueForSubmission !== undefined) {
domainUrlInput.value = punycodeValueForSubmission;
}
// Show the active state
document.getElementById("toAddActive").classList.add("active");
// Disable the button and show loading message
const button = document.getElementById("installButton");
button.disabled = true;
button.innerText = "Adding...";
// Clear any previous status messages
const statusMessageDiv = document.getElementById("statusMessage");
statusMessageDiv.classList.remove("d-none");
statusMessageDiv.innerText = ""; // Clear status
// Gather form data to send via POST
const formData = new FormData(addDomainForm);
let preventRedirect = document.getElementById('preventRedirectCheckbox').checked;
// Use fetch to submit the form data as a POST request
fetch("/add_domain", {
method: "POST",
body: formData,
})
.then(response => {
if (!response.ok) {
throw new Error("Network response was not ok.");
document.getElementById("toAddActive").classList.remove("active");
}
return response.body;
})
.then(stream => {
const reader = stream.getReader();
const decoder = new TextDecoder();
return new ReadableStream({
start(controller) {
function read() {
reader.read().then(({ done, value }) => {
if (done) {
controller.close();
return;
}
const text = decoder.decode(value);
statusMessageDiv.innerText += text; // Append new message
statusMessageDiv.scrollTop = statusMessageDiv.scrollHeight; // Auto scroll
// Check if the text contains "added successfully"
if (text.toLowerCase().includes("added successfully")) {
success = true; // Set success to true if found
}
read(); // Continue reading
}).catch(err => {
console.error("Error reading stream", err);
});
}
read();
}
});
})
.then(() => {
document.getElementById("toAddActive").classList.remove("active");
showCompletion(); // done
if (!preventRedirect) {
window.location.href = '/domains';
} else {
console.log('Staying on the same page.');
}
})
.catch(error => {
console.error("Fetch error: ", error);
//statusMessageDiv.innerText += "Error occurred during the domain addition process.\n";
//document.getElementById("toAddActive").classList.remove("active");
});
// Function to handle completion
function showCompletion() {
statusMessageDiv.style.display = "block";
button.disabled = false;
button.innerText = "Add Domain";
}
}
});
// Function to extract domain name from a URL
function extractDomainName(url) {
let domain = url;
if (domain.indexOf("://") > -1) {
domain = domain.split('/')[2];
} else {
domain = domain.split('/')[0];
}
domain = domain.split(':')[0]; // Remove port if present
return domain;
}
// Function to check if the provided string is a valid domain name
function isValidDomainName(domain) {
const domainRegex = /^(?:(?!-|_)(?:xn--[a-z0-9-]+|[\p{L}\p{M}\p{N}\p{Pc}\p{Lm}]{1,63})(?<!-|_)\.){1,}(?:xn--[a-z0-9-]+|[a-z]{2,})$/u;
return domainRegex.test(domain);
}
// Toggle display of "Add New Domain" form
const showAddDomainFormBtn = document.getElementById("showAddDomainFormBtn");
showAddDomainFormBtn.addEventListener("click", function() {
addDomainForm.style.display = addDomainForm.style.display === "none" ? "block" : "none";
});
function toggleAddDomainForm() {
const currentFragment = window.location.hash;
const addNewFragment = "#add-new";
if (currentFragment === addNewFragment) {
addDomainForm.style.display = "block";
} else {
addDomainForm.style.display = "none";
}
}
// Listen for changes in the URL's fragment identifier
window.addEventListener("hashchange", toggleAddDomainForm);
// Check the initial fragment identifier when the page loads
window.addEventListener("load", toggleAddDomainForm);
// Functions to handle showing/hiding redirect options for each domain
function showRedirectInput(domainId) {
document.getElementById('create-button-' + domainId).style.display = 'none';
document.getElementById('create-form-' + domainId).style.display = 'block';
}
function cancelCreate(domainId) {
document.getElementById('create-form-' + domainId).style.display = 'none';
document.getElementById('create-button-' + domainId).style.display = 'inline';
}
function editRedirect(domainId) {
const spanElement = document.getElementById('redirect-value-' + domainId);
if (spanElement) spanElement.style.display = 'none';
const editButton = document.querySelector('#edit-form-' + domainId + ' button');
if (editButton) editButton.style.display = 'inline';
const editForm = document.getElementById('edit-form-' + domainId);
if (editForm) editForm.style.display = 'block';
}
function cancelEdit(domainId) {
const spanElement = document.getElementById('redirect-value-' + domainId);
if (spanElement) spanElement.style.display = 'inline';
const editButton = document.querySelector('#edit-form-' + domainId + ' button');
if (editButton) editButton.style.display = 'none';
const editForm = document.getElementById('edit-form-' + domainId);
if (editForm) editForm.style.display = 'none';
}
// Function to set domainId and domainName for deletion confirmation
const deleteConfirmationModal = document.getElementById('deleteConfirmationModal');
function setDeleteDomain(domainId, domainName) {
// Set the domainId for deletion confirmation
document.getElementById('deleteDomainId').value = domainId;
// Set the domainName in the modal message
const confirmMessage = deleteConfirmationModal.querySelector('.confirm_message');
const errorMessage = deleteConfirmationModal.querySelector('.error_message');
confirmMessage.innerHTML = `{{ _('Are you sure you want to delete the domain') }} <span class="punycode">${domainName}</span>?`;
// Check if there are associated websites
const associatedWebsites = getAssociatedWebsites(domainName);
// Display associated websites in the modal content
const websiteList = deleteConfirmationModal.querySelector('.website-list');
if (associatedWebsites.length > 0) {
// Disable the "Delete" button
document.getElementById('deleteButton').disabled = true;
// Display message!
errorMessage.innerHTML = `<div class="modal-icon modal-error modal-icon-show"></div>`;
confirmMessage.innerHTML = `<div class="alert alert-danger" role="alert">{{ _('Domain') }} <span class="punycode">${domainName}</span> {{ _('can not be deleted because it is used on websites:') }}</div>`;
websiteList.innerHTML = '';
associatedWebsites.forEach((website) => {
const websiteItem = document.createElement('li');
const websiteLink = document.createElement('a');
websiteLink.textContent = website;
websiteLink.setAttribute('href', `/website?domain=${website}`);
websiteLink.addEventListener('click', function() {
// Simulate a click on the "Cancel" button to close modal first..
const cancelButton = deleteConfirmationModal.querySelector('.btn-secondary');
cancelButton.click();
});
websiteItem.appendChild(websiteLink);
websiteList.appendChild(websiteItem);
});
} else {
errorMessage.innerHTML = `<div class="modal-icon modal-warning modal-icon-show"></div>`;
websiteList.innerHTML = ''; // Clear the website list if there are no associated websites
document.getElementById('deleteButton').disabled = false;
}
// Show the deletion confirmation modal
deleteConfirmationModal.show();
}
// Function to get associated websites for a domain
function getAssociatedWebsites(domainName) {
const links = document.querySelectorAll('.dropdown-item');
const associatedWebsites = [];
links.forEach((link) => {
const href = link.getAttribute('href');
if (href && (href.includes(`/website?domain=${domainName}`) || href.includes(`/pm?domain=${domainName}`))) {
const websitePath = href.replace('/website?domain=', '').replace('/pm?domain=', '');
associatedWebsites.push(websitePath);
}
});
return associatedWebsites;
}
// Close deletion confirmation modal on cancel
const cancelButton = deleteConfirmationModal.querySelector('.btn-secondary');
cancelButton.addEventListener('click', function () {
deleteConfirmationModal.hide();
});
</script>
<script>
const docRootInput = document.getElementById('doc_root');
const punycodeInfo = document.getElementById('punycode-info');
// Variable to hold Punycode value for submission
let punycodeValueForSubmission;
// Add an event listener to the domain_url input field
domainUrlInput.addEventListener('input', function () {
// Get the current value of domain_url
var domainUrlValue = domainUrlInput.value;
// Check if the domain name contains non-ASCII characters
var needsPunycode = /[^\x00-\x7F]/.test(domainUrlValue);
if (needsPunycode) {
// Convert the domain name to Punycode
punycodeValueForSubmission = punycode.toASCII(domainUrlValue);
// Display the Punycode information
punycodeInfo.innerHTML = "{{ _('Converted to Punycode:') }} <b>" + punycodeValueForSubmission + "</b> {{ _('(for compatibility with internationalized domain names)') }}";
punycodeInfo.style.display = 'block';
// Update the doc_root field by replacing 'DOMEN' with the Punycode value
var updatedDocRootValue = "/home/{{current_username}}/" + punycodeValueForSubmission;
docRootInput.value = updatedDocRootValue;
} else {
// If no conversion is needed, hide the Punycode info div
punycodeInfo.style.display = 'none';
// Update the doc_root field without Punycode conversion
var updatedDocRootValue = "/home/{{current_username}}/" + domainUrlValue;
docRootInput.value = updatedDocRootValue;
// Clear the Punycode value for submission
punycodeValueForSubmission = undefined;
}
});
</script>
<script>
document.addEventListener("DOMContentLoaded", function() {
const showAddDomainFormBtn = document.querySelector("#showAddDomainFormBtn");
const domainUrlInput = document.querySelector("#domain_url");
showAddDomainFormBtn.addEventListener("click", function() {
// Set focus to the domain_url input field
domainUrlInput.focus();
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,752 @@
{% extends 'base.html' %}
{% block content %}
<div>
{% if domain %}
<!-- Content to display when domain is provided -->
<style>
.header { position: sticky; top:-1px; z-index:1;}
</style>
<p>{{ _('The DNS Zone Editor feature allows you to create, edit, and delete Domain Name System (DNS) zone records.') }}</p>
<input type="text" id="serial_number_current" value="{{ serial }}" hidden>
{% if view_mode == 'table' %}
<script type="module">
async function DomainNewRow() {
// Get references to elements
const addRecordButton = document.getElementById("AddDNSRecord");
const addRecordRow = document.getElementById("addRecordRow");
// Hide the addRecordRow initially
addRecordRow.style.display = "none";
// Toggle the visibility of addRecordRow when the button is clicked
addRecordButton.addEventListener("click", function() {
if (addRecordRow.style.display === "none") {
addRecordRow.style.display = "table-row";
} else {
addRecordRow.style.display = "none";
}
});
// Get references to search input and display settings div
const searchDomainInput = document.getElementById("searchDomainInput");
// Get all domain rows to be used for filtering
const domainRows = document.querySelectorAll(".domain_row");
// Handle search input changes
searchDomainInput.addEventListener("input", function() {
const searchTerm = searchDomainInput.value.trim().toLowerCase();
// Loop through domain rows and hide/show based on search term
domainRows.forEach(function(row) {
const domainNameElement = row.querySelector("td:nth-child(1)");
// Check if domainNameElement exists before accessing its textContent
if (domainNameElement) {
const domainName = domainNameElement.textContent.toLowerCase();
const domainUrl = row.querySelector("td:nth-child(4)").textContent.toLowerCase();
// Show row if search term matches domain name or domain URL, otherwise hide it
if (domainName.includes(searchTerm) || domainUrl.includes(searchTerm)) {
row.style.display = "table-row";
} else {
row.style.display = "none";
}
}
});
});
// Get reference to the "CancelDNSRecord" button
const cancelDNSRecordButton = document.getElementById("CancelDNSRecord");
// Handle click event on the "CancelDNSRecord" button
cancelDNSRecordButton.addEventListener("click", function() {
// Hide the addRecordRow
addRecordRow.style.display = "none";
// Clear input fields
const inputFields = addRecordRow.querySelectorAll("input");
inputFields.forEach(function(inputField) {
inputField.value = "";
});
});
};
DomainNewRow();
</script>
<style>
thead {
border: 1px solid rgb(90 86 86 / 11%);
}
th {
text-transform: uppercase;
font-weight: 400;
}
</style>
<!-- Search bar -->
<div class="input-group mb-3" style="padding-left:0px;padding-right:0px;">
<input type="text" class="form-control" placeholder="{{ _('Search records') }}" id="searchDomainInput">
</div>
<table class="table">
<thead>
<tr>
<th class="header">{{ _('Name') }}</th>
<th class="header">{{ _('TTL') }}</th>
<th class="header">{{ _('Type') }}</th>
<th class="header">{{ _('Record') }}</th>
<th class="header">{{ _('Actions') }}</th>
</tr>
</thead>
<tbody>
<tr class="hidden" id="addRecordRow" style="display: none; background: #3c63bb17;">
<form method="POST" action="/domains/dns/add-record/">
<td>
<input type="text" name="Name" class="form-control" placeholder="" aria-label="{{ _('Name') }}" aria-describedby="Name" required pattern="^[A-Za-z0-9.@_-]+$">
</td>
<td>
<input type="number" name="TTL" class="form-control" placeholder="" aria-label="{{ _('TTL') }}" aria-describedby="TTL" required min="60" value="14400">
</td>
<td>
<select name="Type" class="form-select" required onchange="updateForm()">
<option value="A">{{ _('A') }}</option>
<option value="AAAA">{{ _('AAAA') }}</option>
<option value="CAA">{{ _('CAA') }}</option>
<option value="CNAME">{{ _('CNAME') }}</option>
<option value="MX">{{ _('MX') }}</option>
<option value="SRV">{{ _('SRV') }}</option>
<option value="TXT">{{ _('TXT') }}</option>
</select>
<input type="text" name="IN" class="form-control" placeholder="" aria-label="{{ _('IN') }}" aria-describedby="IN" required value="IN" hidden>
</td>
<td>
<input type="text" name="Priority" class="form-control" placeholder="0" aria-label="{{ _('Priority') }}" aria-describedby="Priority" style="display:none;">
<input type="text" name="Record" class="form-control" placeholder="" aria-label="{{ _('Record') }}" aria-describedby="Record" required>
<input type="text" name="Domain" class="form-control" placeholder="" aria-label="" aria-describedby="" required value="{{domain}}" hidden>
</td>
<td>
<button type="submit" class="btn btn-primary" id="save-row">{{ _('Save Record') }}</button>
<button type="button" class="btn btn-transparent cancel-row" id="CancelDNSRecord" style="">{{ _('Cancel') }}</button>
</td>
</form>
</tr>
<script>
function updateForm() {
var selectedType = document.getElementsByName("Type")[0].value;
var recordInput = document.getElementsByName("Record")[0];
var priorityInput = document.getElementsByName("Priority")[0];
if (selectedType === "A") {
recordInput.placeholder = "IPv4 {{ _('Address') }}";
recordInput.pattern = "^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$";
} else if (selectedType === "AAAA") {
recordInput.placeholder = "IPv6 {{ _('Address') }}";
recordInput.pattern = "^(?:(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}|(?:(?:[0-9A-Fa-f]{1,4}:){6}|::" +
"(?:[0-9A-Fa-f]{1,4}:){5}|(?:[0-9A-Fa-f]{1,4}:){4}(?::[0-9A-Fa-f]{1,4})?" +
"|(?:[0-9A-Fa-f]{1,4}:){3}(?::[0-9A-Fa-f]{1,4}){0,2}|(?:[0-9A-Fa-f]{1,4}:){2}" +
"(?::[0-9A-Fa-f]{1,4}){0,3}|[0-9A-Fa-f]{1,4}:(?:(?::[0-9A-Fa-f]{1,4}){0,4}|" +
":...(?::[0-9A-Fa-f]{1,4}){0,3})|:(?:(?::[0-9A-Fa-f]{1,4}){0,5}|" +
":...(?::[0-9A-Fa-f]{1,4}){0,2})|::(?:(?::[0-9A-Fa-f]{1,4}){0,6}|" +
":...[0-9A-Fa-f]{1,4}))$";
} else if (selectedType === "CNAME") {
recordInput.placeholder = "{{ _('Domain') }}";
recordInput.pattern = "^[A-Za-z0-9.-]+$";
} else if (selectedType === "TXT") {
recordInput.removeAttribute("pattern");
} else {
recordInput.placeholder = "";
recordInput.pattern = "";
}
if (selectedType === "MX") {
priorityInput.style.display = "block";
recordInput.placeholder = "{{ _('Domain') }}";
recordInput.pattern = "^[A-Za-z0-9.-]+$";
priorityInput.value = "0";
priorityInput.required = true;
} else {
priorityInput.style.display = "none";
priorityInput.required = false;
}
}
// Initial call to set up the form based on the default value of the Type dropdown
updateForm();
</script>
{% for item in dnszone_with_line_numbers %}
{% if item.line %}
<tr class="domain_row">
<!-- Split the line part into values -->
{% set values = item.line.split(maxsplit=4) %}
<!-- Check if 'SOA' is not present in the third value -->
{% if 'SOA' not in values[3] %}
<td>
<div class="row" style="align-content: space-between;">
<span class="col" style="font-weight: bold;">{{ values[0] if values|length > 0 else '' }}</span>
{% if item.comment %}
<span class="col-auto" data-bs-toggle="tooltip" data-bs-placement="top" title="{{ item.comment }}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M8 9h8"></path>
<path d="M8 13h6"></path>
<path d="M9 18h-3a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-3l-3 3l-3 -3z"></path>
</svg>
</span>
{% endif %}
</div>
</td>
<td>{{ values[1] if values|length > 1 else '' }}</td>
<td>{{ values[3] if values|length > 3 else '' }}</td>
<td style="font-weight: bold; width: 50%; overflow: hidden !important; text-overflow: ellipsis; word-break: break-all;">
{% if values|length > 4 %}
{% set last_value = values[4] %}
{{ last_value.strip('"') if last_value.startswith('"') and last_value.endswith('"') else values[4:]|join(' ') }}
{% else %}
{{ values[4] if values|length > 4 else '' }}
{% endif %}
</td>
<td>
<button class="btn btn-outline-primary edit-button" data-line="{{ item.line }}" data-line-number="{{ item.line_number }}" data-domain="{{ domain }}">
<i class="bi bi-pencil-fill"></i><span class="desktop-only">{{ _(' Edit') }}</span>
</button>
&nbsp
<button class="btn btn-outline-danger delete-button" data-row-id="{{ item.line_number }}" data-domain="{{ domain }}">
<i class="bi bi-trash3"></i><span class="desktop-only"> Delete</span>
</button>
<button class="btn btn-outline-success save-button" style="display: none;" data-line-number="{{ item.line_number }}" data-domain="{{ domain }}">
<i class="bi bi-check"></i><span class="desktop-only">{{ _(' Save') }}</span>
</button>
<button class="btn btn-outline-danger cancel-button" style="display: none;" data-line="{{ item.line }}">
<i class="bi bi-x"></i><span class="desktop-only">{{ _(' Cancel') }}</span>
</button>
</td>
{% endif %}
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
<br>
<!-- Modal -->
<div class="modal fade" id="addRecordModal" tabindex="-1" aria-labelledby="addRecordModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addRecordModalLabel">{{ _('Add DNS Record') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ _('Close') }}"></button>
</div>
<div class="modal-body">
<form method="POST" action="{{ url_for('add_dns_record_to_zone') }}">
<div class="mb-3">
<label for="Name" class="form-label">{{ _('Name') }}</label>
<input type="text" name="Name" class="form-control" id="Name" placeholder="{{ _('Name') }}" aria-label="{{ _('Name') }}" aria-describedby="Name" required pattern="^[A-Za-z0-9.@_]+$">
</div>
<div class="mb-3">
<label for="TTL" class="form-label">{{ _('TTL') }}</label>
<input type="number" name="TTL" class="form-control" id="TTL" placeholder="{{ _('TTL') }}" aria-label="{{ _('TTL') }}" aria-describedby="TTL" required min="60" value="14400">
</div>
<div class="mb-3">
<label for="Type" class="form-label">{{ _('Type') }}</label>
<select name="Type" class="form-select" id="TypeModal" required onchange="updateModalForm()">
<option value="A">A</option>
<option value="AAAA">AAAA</option>
<option value="CAA">CAA</option>
<option value="CNAME">CNAME</option>
<option value="MX">MX</option>
<option value="SRV">SRV</option>
<option value="TXT">TXT</option>
</select>
</div>
<input type="text" name="IN" class="form-control" value="IN" hidden>
<div class="mb-3">
<label for="Priority" id="priorityInputDescription" class="form-label" style="display:none;">{{ _('Priority') }}</label>
<input type="text" name="Priority" class="form-control" id="PriorityModal" placeholder="0" aria-label="{{ _('Priority') }}" aria-describedby="Priority" style="display:none;">
</div>
<div class="mb-3">
<label for="Record" class="form-label">{{ _('Record') }}</label>
<input type="text" name="Record" class="form-control" id="RecordModal" placeholder="{{ _('Record') }}" aria-label="{{ _('Record') }}" aria-describedby="Record" required>
</div>
<input type="text" name="Domain" class="form-control" value="{{ domain }}" hidden>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">{{ _('Save Record') }}</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ _('Cancel') }}</button>
</div>
</form>
</div>
</div>
</div>
</div>
<script>
function updateModalForm() {
var selectedType = document.getElementById("TypeModal").value;
var recordInput = document.getElementById("RecordModal");
var priorityInput = document.getElementById("PriorityModal");
var priorityInputDescription = document.getElementById("priorityInputDescription");
if (selectedType === "A") {
recordInput.placeholder = "{{ _('IPv4 Address') }}";
recordInput.pattern = "^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$";
} else if (selectedType === "AAAA") {
recordInput.placeholder = "IPv6 {{ _('Address') }}";
recordInput.pattern = "^(?:(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}|(?:(?:[0-9A-Fa-f]{1,4}:){6}|::" +
"(?:[0-9A-Fa-f]{1,4}:){5}|(?:[0-9A-Fa-f]{1,4}:){4}(?::[0-9A-Fa-f]{1,4})?" +
"|(?:[0-9A-Fa-f]{1,4}:){3}(?::[0-9A-Fa-f]{1,4}){0,2}|(?:[0-9A-Fa-f]{1,4}:){2}" +
"(?::[0-9A-Fa-f]{1,4}){0,3}|[0-9A-Fa-f]{1,4}:(?:(?::[0-9A-Fa-f]{1,4}){0,4}|" +
":...(?::[0-9A-Fa-f]{1,4}){0,3})|:(?:(?::[0-9A-Fa-f]{1,4}){0,5}|" +
":...(?::[0-9A-Fa-f]{1,4}){0,2})|::(?:(?::[0-9A-Fa-f]{1,4}){0,6}|" +
":...[0-9A-Fa-f]{1,4}))$";
} else if (selectedType === "CNAME") {
recordInput.placeholder = "{{ _('Domain') }}";
recordInput.pattern = "^[A-Za-z0-9.-]+$";
// Check if CNAME record already exists
var cnameName = recordInput.value;
if (cnameName) {
checkCnameExists(cnameName);
}
} else {
recordInput.placeholder = "";
//recordInput.pattern = "";
recordInput.removeAttribute("pattern"); # fixes validation bug on txt
}
if (selectedType === "MX") {
priorityInput.style.display = "block";
priorityInputDescription.style.display = "block";
priorityInput.required = true;
} else {
priorityInput.style.display = "none";
priorityInputDescription.style.display = "none";
priorityInput.required = false;
}
function checkCnameExists(cnameName) {
fetch(`/check_cname_exists/${encodeURIComponent(cnameName)}`)
.then(response => response.json())
.then(data => {
if (data.exists) {
alert("CNAME record with this name already exists.");
document.getElementById("RecordModal").value = '';
}
})
.catch(error => {
console.error('Error:', error);
});
}
}
</script>
</section>
<footer class="main-footer btn-toolbar" role="toolbar">
<div class="btn-group" role="group" aria-label="{{ _('Advanced') }}">
<a type="button" href="/domains/edit-dns-zone/{{domain}}?view=code" class="btn">
<i class="bi bi-pencil"></i> <span class="desktop-only">{{ _('Advanced') }} </span>{{ _('Editor') }}
</a>
</div>
<div class="ms-auto" role="group" aria-label="{{ _('Actions') }}">
<a type="button" href="/domains/export-dns-zone/{{domain}}" target="_blank" class="btn" id="ExportDNSZone">
<i class="bi bi-file-earmark-arrow-down"></i> <span class="desktop-only">{{ _('Export') }} </span>{{ _('Zone') }}
</a>
<button type="button" class="btn btn-primary" id="AddDNSRecord">
<i class="bi bi-plus-lg"></i> {{ _('Add') }}<span class="desktop-only">{{ _('Record') }}</span>
</button>
</div>
</footer>
<script>
$(document).ready(function() {
// Handle the delete button click
$('.delete-button').click(function() {
var deleteButton = $(this);
var rowId = deleteButton.data('row-id');
var domain = deleteButton.data('domain');
var rowToDelete = deleteButton.closest('tr');
// Check if the button text is "Confirm"
if (deleteButton.text() === "Confirm") {
// Send the delete request
sendDeleteRequest(rowId, domain, rowToDelete, deleteButton);
} else {
// Change the button text to "Confirm"
deleteButton.text("{{ _('Confirm') }}");
// Change the button class to remove "outline-" part
deleteButton.removeClass('btn-outline-danger').addClass('btn-danger');
// Remove the "bi bi-trash3" icon
deleteButton.find('i').remove();
// Set a timer to reset the button back to "Delete" after 5 seconds
setTimeout(function() {
// Reset the button text to "Delete"
deleteButton.text("{{ _(' Delete') }}");
// Change the button class back to the original
deleteButton.removeClass('btn-danger').addClass('btn-outline-danger');
// Add back the "bi bi-trash3" icon
deleteButton.prepend('<i class="bi bi-trash3"></i>');
}, 5000);
}
});
function sendDeleteRequest(rowId, domain, rowToDelete, deleteButton) {
$.post(`/domains/dns/delete-record/${rowId - 1}`, { domain: domain }, function(data) {
if (data.message === "Row deleted successfully") {
// Remove the row from the table
rowToDelete.remove();
window.location.reload();
}
});
}
// When the "Edit" button is clicked
$('.edit-button').click(function() {
var row = $(this).closest('.domain_row');
var line = $(this).data('line');
var lineNumber = $(this).data('line-number');
var domain = $(this).data('domain');
// Save the original line content
row.data('original-line', line);
// Extract the existing values from the table cells
var existingField1 = row.find('td:eq(0)').text().trim().replace(/\.$/, '');
var existingField2 = row.find('td:eq(1)').text();
var existingField3 = row.find('td:eq(2)').text();
var existingField4 = row.find('td:eq(3)').text().trim().replace(/\.$/, '');
// Replace the row content with input fields for editing and populate with existing data
row.html(`
<td><input type="text" class="form-control" name="field1" value="${existingField1}"></td>
<td><input type="text" class="form-control" name="field2" value="${existingField2}"></td>
<td>
<select name="field3" class="form-select" id="Type" required onchange="updateForm()">
<option value="A" ${existingField3 === 'A' ? 'selected' : ''}>A</option>
<option value="AAAA" ${existingField3 === 'AAAA' ? 'selected' : ''}>AAAA</option>
<option value="CAA" ${existingField3 === 'CAA' ? 'selected' : ''}>CAA</option>
<option value="CNAME" ${existingField3 === 'CNAME' ? 'selected' : ''}>CNAME</option>
<option value="MX" ${existingField3 === 'MX' ? 'selected' : ''}>MX</option>
<option value="SRV" ${existingField3 === 'SRV' ? 'selected' : ''}>SRV</option>
<option value="TXT" ${existingField3 === 'TXT' ? 'selected' : ''}>TXT</option>
</select>
</td>
<td><input type="text" class="form-control" name="field4" id="Record" value="${existingField4}"></td>
<td>
<button class="btn btn-outline-success save-button" data-line-number="${lineNumber}" data-domain="${domain}"><i class="bi bi-check"></i><span class="desktop-only">{{ _(' Save') }}</span></button>
<button class="btn btn-outline-danger cancel-button" data-line="${line}"><i class="bi bi-x"></i><span class="desktop-only">{{ _(' Cancel') }}</span></button>
</td>
`);
// Hide the "Edit" button and show the "Save" and "Cancel" buttons
row.find('.edit-button').hide();
row.find('.save-button, .cancel-button').show();
});
});
$(document).on('click', '.cancel-button', function() {
var row = $(this).closest('.domain_row');
var originalLine = row.data('original-line');
// Use a regular expression to split the original line into components
var originalLineArray = originalLine.match(/\S+/g);
if (originalLineArray && originalLineArray.length >= 4) {
// Restore the original text content of the row
row.find('td:eq(0)').html('<strong>' + originalLineArray[0] + '</strong>');
row.find('td:eq(1)').text(originalLineArray[1]);
row.find('td:eq(2)').text(originalLineArray[3]);
row.find('td:eq(3)').html('<strong>' + originalLineArray[4] + '</strong>');
// Show the "Edit" button and hide the "Save" and "Cancel" buttons
row.find('.edit-button').show();
row.find('.save-button').hide();
row.find('.cancel-button').hide();
row.find('.delete-button').show();
// jer mi ne vrca dugmice..
window.location.reload();
}
});
$(document).on('click', '.save-button', function() {
console.log("Updating record..");
let btnClass, toastMessage;
var row = $(this).closest('.domain_row');
var lineNumber = $(this).data('line-number');
var domain = $(this).data('domain');
var updatedField1 = row.find('input[name="field1"]').val();
var updatedField2 = row.find('input[name="field2"]').val();
var updatedField3 = row.find('select[name="field3"]').val();
var updatedField4 = row.find('input[name="field4"]').val();
// Add a period (.) after record name if it ends with the value of data('domain')
if (updatedField1.endsWith(domain)) {
updatedField1 += '.';
}
// Add a period (.) after record value if it ends with the value of data('domain')
if (updatedField4.endsWith(domain)) {
updatedField4 += '.';
}
// If updatedField3 is CNAME, check for duplicates
if (updatedField3 === 'CNAME') {
let duplicateFound = false;
$('.domain_row').each(function() {
var existingField1 = $(this).find('td:first-child').text().trim(); // Get the first column value
var existingField3 = $(this).find('td:nth-child(3)').text().trim(); // Get the type (CNAME)
// Check if it's a CNAME and has the same first column as updatedField1
if (existingField3 === 'CNAME' && existingField1 === updatedField1) {
duplicateFound = true;
return false; // Break out of the loop
}
});
if (duplicateFound) {
toastMessage = "A CNAME record with this name already exists.";
btnClass = 'danger';
const toast = toaster({
body: toastMessage,
className: `border-0 text-white bg-${btnClass}`,
});
return; // Stop further execution
}
}
// Merge the updated values into a single line separated by spaces
var updatedLine = updatedField1 + ' ' + updatedField2 + ' ' + 'IN' + ' ' + updatedField3 + ' ' + updatedField4;
// Perform an AJAX POST request to update the DNS record
$.post('/domains/dns/update-record/' + lineNumber + '/' + domain,
{
newContent: updatedLine,
serial: $('#serial_number_current').val()
},
function(response) {
if (response.updated_row) {
btnClass = 'success';
toastMessage = "{{ _('Record updated successfully') }}";
// Update the table cell with the new content
row.find('td:eq(0)').text(updatedField1);
row.find('td:eq(1)').text(updatedField2);
row.find('td:eq(2)').text(updatedField3);
row.find('td:eq(3)').text(updatedField4);
// Restore the original line content
row.data('original-line', updatedLine);
// Show the "Edit" button and hide the "Save" and "Cancel" buttons
row.find('.edit-button').show();
row.find('.save-button, .cancel-button').hide();
// TODO! update serial and content only!
window.location.reload();
} else if (response.error) {
let btnClass = 'danger';
let toastMessage = response.error;
}
const toast = toaster({
body: toastMessage,
className: `border-0 text-white bg-${btnClass}`,
});
}).fail(function(xhr) {
// Handle HTTP errors
let btnClass = 'danger';
let toastMessage = "An unexpected error occurred. Please try again.";
if (xhr.responseJSON && xhr.responseJSON.message) {
toastMessage = xhr.responseJSON.message;
}
const toast = toaster({
body: toastMessage,
className: `border-0 text-white bg-${btnClass}`,
});
});
});
window.onload = function() {
// Autofocus on the input field
document.getElementById('searchDomainInput').focus();
};
</script>
{% elif view_mode == 'code' %}
<div class="alert alert-warning">
<strong>Attention:</strong> Editing DNS file is intended for advanced users only.
This action can potentially lead to server misconfiguration or downtime if not done correctly.
<br><br>
Please be cautious and make sure you understand the changes you are making.
</div>
<form method="post" action="/domains/save-dns-zone/{{ domain }}">
<textarea style="height:60vh;" class="form-control" id="zone_content" name="zone_content" rows="10" cols="80">{{ zone_content }}</textarea>
</section>
<footer class="main-footer btn-toolbar" role="toolbar">
<div class="btn-group" role="group" aria-label="{{ _('Advanced') }}">
<a type="button" href="/domains/edit-dns-zone/{{domain}}?view=table" class="btn">
<i class="bi bi-arrow-left"></i> <span class="desktop-only">{{ _('Go') }} </span>{{ _('Back') }}
</a>
</div>
<div class="ms-auto" role="group" aria-label="{{ _('Actions') }}">
<button type="button" class="btn btn-primary" id="save_zone_button">
{{ _('Save') }}<span class="desktop-only"> {{ _('Zone') }}</span>
</button>
</form>
</div>
</footer>
<script>
document.getElementById('save_zone_button').addEventListener('click', function() {
const zoneContent = document.getElementById('zone_content').value;
const domain = encodeURIComponent('{{ domain }}');
console.log("Saving zone..");
const params = new URLSearchParams();
params.append('zone_content', zoneContent);
fetch(`/domains/save-dns-zone/${domain}`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: params.toString()
})
.then(response => response.text())
.then(responseText => {
let btnClass, toastMessage;
if (responseText.includes('saved')) {
btnClass = 'success';
toastMessage = "{{ _('Zone saved successfully') }}";
} else {
btnClass = 'danger';
toastMessage = responseText;
}
const toast = toaster({
body: toastMessage,
className: `border-0 text-white bg-${btnClass}`,
});
})
.catch(error => {
console.error('Error:', error);
// Handle error case
btnClass = 'danger';
toastMessage = data.error;
const toast = toaster({
body: toastMessage,
className: `border-0 text-white bg-${btnClass}`,
});
});
});
</script>
{% endif %}
{% else %}
<!-- Content to display when domain is not provided -->
<p>{{ _('DNS Zone Editor allows you to manage and edit Domain Name System (DNS) zone files, which contain critical information for mapping domain names to IP addresses and managing various DNS records, such as A, CNAME, MX, and TXT records.') }}</p>
<form method="GET">
<div class="input-group mb-3">
<label class="input-group-text" for="php_version">{{ _('Select domain:') }}</label>
<select class="form-select form-select-lg" id="php_version" name="version" onchange="redirectToSelectedVersion(this)">
<option value="" disabled selected>{{ _('Select a domain name to edit DNS Zone') }}</option>
{% for domain in domains %}
<option value="{{ domain.domain_url }}">{{ domain.domain_url }}</option>
{% endfor %}
</select>
</div>
</form>
<script>
// JavaScript function to redirect to the same page with domain parameter
function redirectToSelectedVersion(selectElement) {
var selectedVersion = selectElement.value;
if (selectedVersion) {
window.location.href = `/domains/edit-dns-zone/${selectedVersion}`;
}
}
</script>
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,19 @@
{% extends 'base.html' %}
{% block content %}
<div>
<div class="alert alert-primary">
<strong>Attention:</strong> Editing domain file is intended for advanced users only. This action can potentially lead to server misconfiguration or downtime if not done correctly.
<br><br>
Please be cautious and make sure you understand the changes you are making. We use a rollback mechanism to check for errors, and if any issues are detected, we will revert the changes. However, it's important to have a backup of your configurations before proceeding.
</div>
<form action="/domains/save-vhosts" method="post">
<input type="hidden" name="domain_name" value="{{ domain_name }}">
<label for="vhost_content">Virtual Host Configuration:</label>
<div class="form-group">
<textarea class="form-control" name="vhost_content" rows="28">{{ vhost_content }}</textarea>
</div>
<button type="submit" class="btn mt-2 btn-lg btn-success">Save</button>
</form>
</div>
{% endblock %}

View File

@@ -0,0 +1,25 @@
{% extends 'base.html' %}
{% block content %}
<div class="row g-3 justify-content-center">
<form method="GET">
<div class="input-group mb-3">
<label class="input-group-text" for="php_version">Select domain:</label>
<select class="form-select form-select-lg" id="php_version" name="version" onchange="redirectToSelectedVersion(this)">
<option value="" disabled selected>Select a domain name to view access logs</option>
{% for domain in domains %}
<option value="{{ domain.username }}/{{ domain.domain_url }}">{{ domain.domain_url }}</option>
{% endfor %}
</select>
</div>
</form>
<script>
function redirectToSelectedVersion(selectElement) {
var selectedVersion = selectElement.value;
if (selectedVersion) {
var url = `/domains/log/${selectedVersion}`;
window.open(url, '_blank');
}
}
</script>
</div>
{% endblock %}

View File

@@ -0,0 +1 @@
{{ html_content|safe }}

View File

@@ -0,0 +1,280 @@
<!-- elasticsearch.html -->
{% extends 'base.html' %}
{% block content %}
<script type="module">
// Function to attach event listeners
function attachEventListeners() {
document.querySelectorAll("button[type='submit']").forEach((btn) => {
if (!btn.classList.contains("limits")) {
btn.addEventListener("click", async (ev) => {
ev.preventDefault();
const action = btn.closest("form").querySelector("input[name='action']").value;
let btnClass, toastMessage;
if (action === 'enable') {
btnClass = 'success';
toastMessage = '{{ _("Enabling Elasticsearch service..") }}';
} else if (action === 'install_elasticsearch') {
btnClass = 'primary';
toastMessage = '{{ _("Installing Elasticsearch service.. Please wait") }}';
} else if (action === 'disable') {
btnClass = 'danger';
toastMessage = '{{ _("Disabling Elasticsearch service..") }}';
}
const toast = toaster({
body: toastMessage,
className: `border-0 text-white bg-${btnClass}`,
});
try {
const response = await fetch(window.location.href, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `action=${action}`,
});
// get the response HTML content
const resultHtml = await response.text();
// Parse the HTML string to extract the content of the specific element
const parser = new DOMParser();
const doc = parser.parseFromString(resultHtml, 'text/html');
const mainScopeContent = doc.getElementById("main-scope")?.innerHTML;
// Replace the content of the element with the ID "main-scope"
const mainScopeElement = document.getElementById("main-scope");
if (mainScopeElement) {
mainScopeElement.innerHTML = mainScopeContent || '';
}
// Reattach event listeners after updating content
attachEventListeners();
// Reinitialize the log script
initializeLogScript();
} catch (error) {
console.error('Error:', error);
}
});
}
});
}
// Function to initialize the log script
function initializeLogScript() {
$(document).ready(function() {
$("#service-log").click(function(event) {
event.preventDefault();
$.ajax({
url: "/view-log/var/log/elasticsearch/elasticsearch.log",
type: "GET",
success: function(data) {
$("#log-content").html(data);
$("#log-container").show(); // Show the container when data is fetched
},
error: function() {
$("#log-content").html("Error fetching log content.");
$("#log-container").show(); // Show the container even on error
}
});
});
});
}
// Attach event listeners initially
attachEventListeners();
</script>
<div class="row g-3">
{% if elastic_status_display == 'ON' %}
<div class="col-md-4 col-xl-4">
<div class="card card-one">
<div class="card-header">
<h6 class="card-title">{{_('Connection Info')}}</h6>
</div>
<div class="card-body">
<div class="row mt-2 mb-2">
<label class="card-title fw-medium text-dark mb-1">{{_('status:')}}</label><div class="col-6">
<h3 class="card-value mb-1"><i class="bi bi-check-circle-fill"></i> {{_('Active')}}</h3>
</div><!-- col -->
</div>
<hr>
<div class="row mt-2 mb-2">
<div class="col-6">
<label class="card-title fw-medium text-dark mb-1">{{_('Elasticsearch server:')}}</label><h3 class="card-value mb-1">127.0.0.1</h3>
<span class="d-block text-muted fs-11 ff-secondary lh-4">{{_('*or localhost')}}</span>
</div><!-- col -->
</div>
<hr>
<div class="row mt-2 mb-2">
<div class="col-12">
<label class="card-title fw-medium text-dark mb-1">Port:</label>
<h3 class="card-value mb-1">9200</h3><span class="d-block text-muted fs-11 ff-secondary lh-4">{{_('*Access to the service is NOT available from other servers.')}}</span>
</div><!-- col -->
</div><!-- row -->
</div><!-- card-body -->
<div class="card-footer d-flex justify-content-center">
<a href="#" class="fs-sm" id="service-log">{{_('View Elasticsearch service Log')}}</a>
</div>
</div><!-- card-one -->
</div>
<div class="col-md-6 col-xl-8">
<div class="card card-one">
<div class="card-header">
<h6 class="card-title">{{_('Elasticsearch Settings')}}</h6>
<nav class="nav nav-icon nav-icon-sm ms-auto">
<a href="" class="nav-link"><i class="ri-refresh-line"></i></a>
<a href="" class="nav-link"><i class="ri-more-2-fill"></i></a>
</nav>
</div><!-- card-header -->
<div class="card-body">
</div>
</div>
</div><!-- col -->
</div><!-- row -->
<div class="row g-3">
<div class="col-md-6 col-xl-12" style="display: none;" id="log-container">
<div class="card card-one">
<div class="card-header">
<h6 class="card-title">{{_('Elasticsearch service logs')}}</h6>
<nav class="nav nav-icon nav-icon-sm ms-auto"></nav>
</div><!-- card-header -->
<div class="card-body">
<pre id="log-content"></pre>
</div><!-- card-body -->
</div><!-- card -->
</div>
</div>
{% elif elastic_status_display == 'NOT INSTALLED' %}
<div class="jumbotron text-center mt-5" id="jumbotronSection">
<h1><i class="bi bi-x-lg" style="color:red;"></i> {{_('Elasticsearch is not currently installed.')}}</h1>
<p>{{_('To install Elasticsearch click on the button bellow.')}}</p>
<form method="post">
<input type="hidden" name="action" value="install_elasticsearch">
<button class="btn btn-lg btn-primary" type="submit">{{_('INSTALL ELASTICSEARCH')}}</button>
</form>
</div>
{% elif elastic_status_display == 'OFF' %}
<div class="jumbotron text-center mt-5" id="jumbotronSection">
<h1><i class="bi bi-x-lg" style="color:red;"></i> {{_('Elasticsearch is currently disabled.')}}</h1>
<p>{{_('To enable Elasticsearch click on the button bellow.')}}</p>
<form method="post">
<input type="hidden" name="action" value="enable">
<button class="btn btn-lg btn-primary" type="submit">{{_('START ELASTICSEARCH')}}</button>
</form>
</div>
{% else %}
<div class="jumbotron text-center mt-5" id="jumbotronSection">
<h1><i class="bi bi-x-lg"></i> {{_('Elasticsearch service status is unknown.')}}</h1>
<p>{{_('Unable to determinate current elasticsearch service status, try Start&Stop actions.')}} <br>{{_('If the issue persists please contact support.')}}</p>
<form method="post">
<input type="hidden" name="action" value="enable">
<button class="btn btn-primary d-flex align-items-center gap-2" type="submit">{{_('START')}}</button>
</form>
<form method="post">
<input type="hidden" name="action" value="disable">
<button class="btn btn-primary d-flex align-items-center gap-2" type="submit">{{_('STOP')}}</button>
</form>
</div>
{% endif %}
</div>
<script>
$(document).ready(function() {
$("#service-log").click(function(event) {
event.preventDefault();
$.ajax({
url: "/view-log/var/log/elasticsearch/elasticsearch.log",
type: "GET",
success: function(data) {
$("#log-content").html(data);
$("#log-container").show(); // Show the container when data is fetched
},
error: function() {
$("#log-content").html("Error fetching log content.");
$("#log-container").show(); // Show the container even on error
}
});
});
});
</script>
</section>
<footer class="main-footer btn-toolbar" role="toolbar">
<div class="btn-group" role="group" aria-label="Status">
<label>status:</label><b> {% if elastic_status_display == 'ON' %} Enabled{% elif elastic_status_display == 'OFF' %} Disabled{% elif elastic_status_display == 'NOT INSTALLED' %} Not Installed{% else %} Unknown{% endif %}</b>
</div>
<div class="ms-auto" role="group" aria-label="Actions">
{% if elastic_status_display == 'ON' %}
<form method="post">
<input type="hidden" name="action" value="disable">
<button class="btn btn-danger d-flex align-items-center gap-2" type="submit">{{_('Disable Elasticsearch')}}</button>
</form>
{% elif elastic_status_display == 'OFF' %}
<form method="post">
<input type="hidden" name="action" value="enable">
<button class="btn btn-success d-flex align-items-center gap-2" type="submit">{{_('Enable Elasticsearch')}}</button>
</form>
{% elif elastic_status_display == 'NOT INSTALLED' %}
<form method="post">
<input type="hidden" name="action" value="install_elasticsearch">
<button class="btn btn-success d-flex align-items-center gap-2" type="submit">{{_('Install Elasticsearch')}}</button>
</form>
{% else %}
<form method="post">
<input type="hidden" name="action" value="disable">
<button class="btn btn-primary d-flex align-items-center gap-2" type="submit">{{_('Disable Elasticsearch')}}</button>
</form>
<form method="post">
<input type="hidden" name="action" value="enable">
<button class="btn btn-primary d-flex align-items-center gap-2" type="submit">{{_('Enable Elasticsearch')}}</button>
</form>
{% endif %}
</div>
</footer>
{% endblock %}

View File

@@ -0,0 +1,395 @@
{% extends 'base.html' %}
{% block content %}
<style>
td {vertical-align: middle;}
@media (min-width: 768px) {
table.table {
table-layout: fixed;
}
table.table td {
word-wrap: break-word;
}
}
@media (max-width: 767px) {
span.advanced-text {display:none;}
}
.hidden {
display: none;
}
.advanced-settings {
align-items: center;
text-align: right;
color: black;
}
.advanced-settings i {
margin-right: 5px;
transform: rotate(-90deg);
transition: transform 0.3s ease-in-out;
}
[data-bs-theme=light] .domain_link {
color: black;
}
.domain_link {
border-bottom: 1px dashed #999;
text-decoration: none;
}
.advanced-settings.active i {
transform: rotate(0);
}
thead {
border: 1px solid rgb(90 86 86 / 11%);
}
th {
text-transform: uppercase;
font-weight: 400;
}
</style>
<div class="row">
<div class="collapse mb-2" id="addEmailForm" style="display: none;">
<div class="card card-body">
<div class="container">
<a href="#" class="nije-link" id="cancelLinkemails" style="display: none;"><i class="bi bi-x-lg" style="right: 15px;top: 15px;position: absolute;color: black;padding: 6px 10px;border-radius: 50px;"></i></a>
<div class="row">
<div class="col-md-6 offset-md-3">
<h2 class="mb-3"><i class="bi bi-envelope-plus"></i> {{ _("Create new email account") }}</h2>
<p>{{ _("Use this form to create new email addresses for any of the domains on your account.") }}</p>
<form action="/emails" method="post">
<div class="form-group">
<label for="domain" class="form-label" data-toggle="tooltip" data-placement="top" title="{{ _('Choose the domain that you want to use. Your email address will end with this domain (username@domain.com).') }}">{{ _("Domain:") }}</label>
<div class="input-group">
</select>
<select class="form-select" name="domain" id="domain">
{% for domain in domains %}
<option class="punycode" value="{{ domain.domain_url }}">{{ domain.domain_url }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="form-group">
<label for="at_address" data-toggle="tooltip" data-placement="top" title="{{ _('Enter the username that you want to use. Your email address will start with this username (username@domain.com).') }}">{{ _("Username:") }}</label>
<input type="text" class="form-control" name="email_username" required>
<div class="input-group-append" style="width: 100%;">
<input type="text" class="form-control" width="40%" name="at_address" id="at_address" disabled>
</div>
</div>
<script>
$(document).ready(function() {
// Check if the URL contains the parameter "install"
const urlParams = new URLSearchParams(window.location.search);
const installParam = urlParams.get('create');
if (installParam || window.location.hash === '#create') {
// Show the Bootstrap collapsible element
$("#addEmailForm").collapse('show');
}
// Add event listener to the dropdown
$("#domain").change(function() {
// Get the selected domain URL
var selectedDomain = $("#domain option:selected").text();
// Update the admin email input value
var adminEmailInput = $("#at_address");
var currentAdminEmail = adminEmailInput.val();
adminEmailInput.val('@' + selectedDomain);
});
// Add event listener to the "Install WordPress" button to toggle form and jumbotron
$("#addEmailForm").on('shown.bs.collapse', function () {
$("#jumbotronSection").hide();
$("#cancelLinkemails").show();
});
// Add event listener to the "Cancel" link to toggle form and jumbotron
$("#cancelLinkemails").click(function() {
$("#addEmailForm").collapse('hide');
$("#jumbotronSection").show();
$("#cancelLinkemails").hide();
});
});
var selectedDomain = $("#domain option:selected").text();
// Update the admin email input value
var adminEmailInput = $("#at_address");
var currentAdminEmail = adminEmailInput.val();
adminEmailInput.val('@' + selectedDomain);
</script>
<div class="form-group">
<label for="email_password" class="form-label">{{ _("Password:") }}</label>
<div class="input-group">
<input type="password" class="form-control" name="email_password" id="email_password" required>
<div class="input-group-append">
<button class="btn btn-outline-success" type="button" id="generatePassword">
{{ _("Generate") }}
</button>
<button class="btn btn-outline-secondary" type="button" id="togglePassword">
<i class="bi bi-eye"></i>
</button>
</div>
</div>
</div>
<script>
function generateRandomStrongPassword(length) {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
let result = "";
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * charset.length);
result += charset.charAt(randomIndex);
}
return result;
}
function generateInitiallyUsernameAndPassword() {
const generatedPassword = generateRandomStrongPassword(16);
document.getElementById("email_password").value = generatedPassword;
};
generateInitiallyUsernameAndPassword();
document.getElementById("generatePassword").addEventListener("click", function() {
const generatedPassword = generateRandomStrongPassword(16);
document.getElementById("email_password").value = generatedPassword;
const passwordField = document.getElementById("email_password");
if (passwordField.type === "password") {
passwordField.type = "text";
}
});
document.getElementById("togglePassword").addEventListener("click", function() {
const passwordField = document.getElementById("email_password");
if (passwordField.type === "password") {
passwordField.type = "text";
} else {
passwordField.type = "password";
}
});
</script>
<br>
<button type="submit" class="btn btn-primary">{{ _("Create") }}</button>
</form>
</div>
</div>
</div>
</div>
</div>
<table class="table table-hover">
<thead>
<tr>
<th width="">{{ _('Account @ Domain') }}</th>
<th width="" data-toggle="tooltip" data-placement="bottom" title="{{ _('Current disk usage for the email account.') }}">{{ _('Storage') }}<span class="desktop-only">{{ _(' Used') }}</span></th>
<th width="">{{ _('Options') }}</th>
</tr>
</thead>
<tbody>
{% for email_entry in current_emails_list %}
{% set parts = email_entry.split(' ') %}
{% set status = parts[0] %}
{% set address = parts[1] %}
{% set quota = ' '.join(parts[2:]) %}
{% set quota_parts = quota.split('[') %}
{% if quota_parts|length > 1 %}
{% set percentage_str = quota_parts[1].split(']')[0] %}
{% else %}
{% set percentage_str = '0' %}
{% endif %}
<tr class="domain_row">
<td><span class="punycode">{{ address }}</span>
<td>{{ quota }}
<br>
<div class="progress" style="height: 10px;">
<div id="disk-progress-bar" class="progress-bar w-4 bg-success" role="progressbar" aria-valuemin="0" aria-valuemax="100" style="width: {{ percentage_str }};"></div>
</div>
</td>
<td>
<div class="flex">
<a href="/webmail/{{ address }}" class="btn btn-primary btn-sm webmail-link" data-email="{{ address }}" role="button"><i class="bi bi-box-arrow-up-right"></i> Webmail</a>
<a href="/emails/{{ address }}" class="btn btn-outline-primary btn-sm" role="button"><i class="bi bi-wrench-adjustable"></i> Manage</a>
<a href="/emails/connect/{{ address }}" class="btn btn-outline-primary d-none btn-sm" role="button"><i class="bi bi-phone"></i> Connect Devices</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<script>
document.addEventListener("DOMContentLoaded", function() {
// Attach event listener to all webmail links
document.querySelectorAll('.webmail-link').forEach(function(link) {
link.addEventListener('click', function(e) {
e.preventDefault(); // Prevent the default link behavior
const email = this.dataset.email; // Get the email from the data attribute
const url = `/webmail/${email}`; // Construct the URL
let btnClass, toastMessage;
btnClass = 'primary';
toastMessage = "{{ _('Webmail for ') }}" + email + "{{ _(' opened in new window.') }}";
const toast = toaster({
body: toastMessage,
className: `border-0 text-white bg-${btnClass}`,
});
window.open(url, '_blank');
});
});
});
</script>
</section>
<footer class="main-footer btn-toolbar" role="toolbar">
<div class="btn-group" role="group" aria-label="{{ _('Status') }}">
<input type="text" class="form-control" placeholder="{{ _('Search addresses') }}" id="searchDomainInput">
</div>
<div class="ms-auto" role="group" aria-label="{{ _('Actions') }}">
<button type="button" class="btn btn-primary d-flex align-items-center gap-2" id="showAddEmailFormBtn"><i class="bi bi-plus-lg"></i> <span class="desktop-only">{{ _('Create') }}</span></button>
</div>
</footer>
<script>
function initializeDomainManagement() {
// Get references to search input and display settings div
const searchDomainInput = document.getElementById("searchDomainInput");
// Get all domain rows to be used for filtering
const domainRows = document.querySelectorAll(".domain_row");
// Handle search input changes
searchDomainInput.addEventListener("input", function() {
const searchTerm = searchDomainInput.value.trim().toLowerCase();
// Loop through domain rows and hide/show based on search term
domainRows.forEach(function(row) {
const domainName = row.querySelector("td:nth-child(1)").textContent.toLowerCase();
const domainUrl = row.querySelector("td:nth-child(2)").textContent.toLowerCase();
// Show row if search term matches domain name or domain URL, otherwise hide it
if (domainName.includes(searchTerm) || domainUrl.includes(searchTerm)) {
row.style.display = "table-row";
} else {
row.style.display = "none";
}
});
});
}
initializeDomainManagement ();
// Toggle display of "Add New Domain" form
const showAddEmailFormBtn = document.getElementById("showAddEmailFormBtn");
showAddEmailFormBtn.addEventListener("click", function() {
addEmailForm.style.display = addEmailForm.style.display === "none" ? "block" : "none";
});
function toggleEmailDomainForm() {
const currentFragment = window.location.hash;
const addNewFragment = "#create";
if (currentFragment === addNewFragment) {
addEmailForm.style.display = "block";
} else {
addEmailForm.style.display = "none";
}
}
// Listen for changes in the URL's fragment identifier
window.addEventListener("hashchange", toggleEmailDomainForm);
// Check the initial fragment identifier when the page loads
window.addEventListener("load", toggleEmailDomainForm);
</script>
{% endblock %}

View File

@@ -0,0 +1,511 @@
{% extends 'base.html' %}
{% block content %}
{% set parts = current_emails_list.split(' ') %}
{% set status = parts[0] %}
{% set address = parts[1] %}
{% set quota = ' '.join(parts[2:]) %}
{% set quota_parts = quota.split('[') %}
{% if quota_parts|length > 1 %}
{% set percentage_str = quota_parts[1].split(']')[0] %}
{% else %}
{% set percentage_str = '0' %}
{% endif %}
<div class="row">
<div class="card card-body">
<div class="container">
<div class="row">
<div class="col-md-6 offset-md-3">
<h2 class="mb-3"><i class="bi bi-envelope-exclamation"></i> {{ _("Manage an Email Account") }}</h2>
<p>{{ _("Use this page to manage your email account.") }}</p>
<form action="/emails/{{address}}" method="post">
<div class="form-group">
<label for="domain" class="form-label">{{ _("Email Account") }}</label>
<div class="input-group">
</select>
<input type="email" class="form-control" name="email_address" value="{{ address }}" readonly="" required>
</div>
</div>
<div class="form-group">
<label for="usage">{{ _("Current Storage Usage") }}</label>
<div style="width:100%;">
{{ quota }}
<br>
<div class="progress" style="height: 10px;">
<div name="usage" class="progress-bar w-4 bg-success" role="progressbar" aria-valuemin="0" aria-valuemax="100" style="width: {{ percentage_str }};"></div>
</div>
</div>
</div>
<div class="form-group">
<label for="at_address" data-toggle="tooltip" data-placement="top" title="{{ _('The amount of space that your email account can use to store emails.') }}">{{ _("Allocated Storage Space:") }}</label>
<input type="text" class="form-control" name="email_username" value="{{usage_number}}">
<div class="input-group-append">
<select class="form-control" style="width: 60px;" name="gb" id="gb">
<option value="GB" selected>GB</option>
<option value="MB">MB</option>
<option value="TB">TB</option>
<option value="PB">PB</option>
</select>
</div>
</div>
<div class="form-group">
<label for="incoming" class="form-label" data-toggle="tooltip" data-placement="top" title="{{ _('The system will reject incoming email while the account has incoming emails suspended.') }}">{{ _("Receiving Incoming Mail") }}</label>
<div class="form-field" id="incoming">
<div class="form-check">
<input class="form-check-input" type="radio" name="incoming" id="allow_incoming" value="allow" checked="">
<label class="form-check-label" for="allow_incoming">{{ _("Allow") }}</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="incoming" id="suspend_incoming" value="suspend">
<label class="form-check-label" for="suspend_incoming">{{ _("Suspend") }}</label>
</div>
</div>
</div>
<div class="form-group">
<label for="outgoing" class="form-label" data-toggle="tooltip" data-placement="top" title="{{ _('The system will reject outgoing email while the account has outgoing emails suspended.') }}">{{ _("Sending Outgoing Email") }}</label>
<div class="form-field" id="outgoing">
<div class="form-check">
<input class="form-check-input" type="radio" name="outgoing" id="allow_outgoing" value="allow" checked="">
<label class="form-check-label" for="allow_outgoing">{{ _("Allow") }}</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="outgoing" id="suspend_outgoing" value="suspend">
<label class="form-check-label" for="suspend_outgoing">{{ _("Suspend") }}</label>
</div>
</div>
</div>
<div class="form-group">
<label for="email_password" class="form-label" data-toggle="tooltip" data-placement="top" title="{{ _('Set new password for the account or leave empty to not change password.') }}">{{ _("New Password:") }}</label>
<div class="input-group">
<input type="password" class="form-control" name="email_password" id="email_password">
<div class="input-group-append">
<button class="btn btn-outline-success" type="button" id="generatePassword">
{{ _("Generate") }}
</button>
<button class="btn btn-outline-secondary" type="button" id="togglePassword">
<i class="bi bi-eye"></i>
</button>
</div>
</div>
</div>
<script>
function generateRandomStrongPassword(length) {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
let result = "";
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * charset.length);
result += charset.charAt(randomIndex);
}
return result;
}
document.getElementById("generatePassword").addEventListener("click", function() {
const generatedPassword = generateRandomStrongPassword(16);
document.getElementById("email_password").value = generatedPassword;
const passwordField = document.getElementById("email_password");
if (passwordField.type === "password") {
passwordField.type = "text";
}
});
document.getElementById("togglePassword").addEventListener("click", function() {
const passwordField = document.getElementById("email_password");
if (passwordField.type === "password") {
passwordField.type = "text";
} else {
passwordField.type = "password";
}
});
</script>
<br>
<button type="submit" class="btn btn-primary">{{ _("Update Email Settings") }}</button>
</form>
</div>
</div>
</div>
</div>
<div class="accordion" id="accordionExample">
<div class="accordion-item">
<h2 class="accordion-header" id="headingOne">
<!-- it needs the collapsed class to start hidden -->
<button class="accordion-button collapsed" id="get_sieve_data" data-email="{{address}}" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne" aria-controls="collapseOne" aria-expanded="false">
{{ _("Email Filtering") }}
</button>
</h2>
<div id="collapseOne" class="accordion-collapse collapse" aria-labelledby="headingOne" data-bs-parent="#accordionExample" style="">
<div class="accordion-body">
<div class="form-group">
<p class="form-label"><a href="http://sieve.info/documents" target="_blank">Sieve</a> {{ _("allows to specify filtering rules for incoming emails that allow for example sorting mails into different folders depending on the title of an email.") }}</p>
<div class="row">
<div class="col-md-6 col-xl-6"><div class="card card-one"><div class="card-header"><h6 class="card-title">{{ _("Current Filters") }}</h6></div><div class="card-body">
{% set domain = address.split('@')[-1] if '@' in address else 'DOMAIN_NAME' %}
{% set user = address.split('@')[0] if '@' in address else 'USERNAME' %}
<code>/home/{{current_username}}/mail/{{ domain }}/{{user}}/home/.dovecot.sieve</code>
<div class="block">
<textarea id="sieve" name="sieve" rows="20" style="width:100%">
</textarea>
</div>
<script>
document.getElementById('get_sieve_data').addEventListener('click', function() {
var email = this.getAttribute('data-email');
var xhr = new XMLHttpRequest();
xhr.open('GET', '/filters/' + encodeURIComponent(email), true);
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
var response = JSON.parse(xhr.responseText);
document.getElementById('sieve').value = response.content;
} else {
console.error('Failed to retrieve sieve data');
}
};
xhr.send();
});
</script>
<script type="module">
function attachEventListenersForEmailFilter() {
document.querySelectorAll("button.sieve").forEach((btn) => {
btn.addEventListener("click", async (ev) => {
ev.preventDefault(); // Prevent form submission
const address = '{{address}}';
let btnClass, toastMessage;
btnClass = 'primary';
const sieveContent = document.getElementById('sieve').value;
toastMessage = '{{ _("Saving .dovecot.sieve file.. Please wait") }}';
// Show initial saving toast
const toast = toaster({
body: toastMessage,
className: `border-0 text-white bg-${btnClass}`,
});
try {
const response = await fetch(`/filters/${encodeURIComponent(address)}`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `new_content=${encodeURIComponent(sieveContent)}`,
});
if (response.ok) {
// Show success toast
toaster({
body: '{{ _("File saved successfully") }}',
className: 'border-0 text-white bg-success',
});
} else {
// Handle error response
const errorMessage = await response.text();
toaster({
body: `Error: ${errorMessage}`,
className: 'border-0 text-white bg-danger',
});
}
} catch (error) {
// Handle fetch error
toaster({
body: `Error: ${error.message}`,
className: 'border-0 text-white bg-danger',
});
}
});
});
}
attachEventListenersForEmailFilter();
</script>
<br>
<button type="button" class="btn btn-primary sieve">{{ _("Save Filters") }}</button>
</div></div></div>
<div class="col-md-6 col-xl-6"><div class="card card-one"><div class="card-header"><h6 class="card-title">{{ _("Examples") }}</h6></div><div class="card-body">
<form>
<div class="form-group">
<div class="form-field">
<p>{{ _("An example of a sieve filter that moves mails to a folder ") }}<code>INBOX/spam</code> {{ _("depending on the sender address:") }}</p>
<pre>
require ["fileinto", "reject"];
if address :contains ["From"] "spam@spam.com" {
fileinto "INBOX.spam";
} else {
keep;
}
</pre>
</div>
</div>
<div class="form-group">
<div class="form-field">
<p>{{ _("Another example of a sieve filter that forward mails to a different address:") }}</p>
<pre>
require ["copy"];
redirect :copy "user2@not-example.com";
</pre>
</div>
</div>
<div class="form-group">
<div class="form-field">
<p>{{ _("Just forward all incoming emails and do not save them locally:") }}</p>
<pre>
redirect "user2@not-example.com";
</pre>
</div>
</div>
<div class="form-group">
<div class="form-field">
<p>{{ _("It is possible to sort subaddresses such as") }} <code>user+mailing-lists@example.com</code> {{ _("into a corresponding folder") }} (here: <code>INBOX/Mailing-lists</code>) {{ _("automatically.") }}</p>
<pre>
require ["envelope", "fileinto", "mailbox", "subaddress", "variables"];
if envelope :detail :matches "to" "*" {
set :lower :upperfirst "tag" "${1}";
if mailboxexists "INBOX.${1}" {
fileinto "INBOX.${1}";
} else {
fileinto :create "INBOX.${tag}";
}
}
</pre>
</div>
</div>
</form>
</div></div></div>
</div>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="headingTwo">
<!-- it needs the collapsed class to start hidden -->
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseTwo" aria-controls="collapseTwo" aria-expanded="false">
{{ _("Mail Client Configuration") }}
</button>
</h2>
<div id="collapseTwo" class="accordion-collapse collapse" aria-labelledby="headingTwo" data-bs-parent="#accordionExample" style="">
<div class="accordion-body">
<div class="form-group">
<p class="form-label">{{ _("You can manually configure your mail client using the settings below. We recommend that you use IMAP and SMTP for your email account rather than ActiveSync unless you are on Android and need contacts support or push updates.") }}</p>
<div class="row">
<div class="col-md-6 col-xl-6"><div class="card card-one"><div class="card-header"><h6 class="card-title">{{ _("Secure SSL/TLS Settings (Recommended)") }}</h6></div><div class="card-body">
<form>
<div class="form-group">
<label for="readOnlyFieldReal" class="form-label">Username:</label>
<div class="form-field">
<input type="text" value="{{address}}" class="form-control" readonly="">
</div>
</div>
<div class="form-group">
<label for="readOnlyFieldReal" class="form-label">Password:</label>
<div class="form-field">
<i>Use the email accounts password.</i>
</div>
</div>
<div class="form-group">
<label for="readOnlyFieldReal" class="form-label">Incoming Server:</label>
<div class="form-field">
<input type="text" value='{% if dedicated_ip != _("Unknown") %}{{ dedicated_ip }}{% else %}{{ server_ip }}{% endif %}' class="form-control" readonly="">
<div id="imapport" class="form-text">IMAP Port: <b>993</b></div>
</div>
</div>
<div class="form-group">
<label for="readOnlyFieldReal" class="form-label">Outgoing Server:</label>
<div class="form-field">
<input type="text" value='{% if dedicated_ip != _("Unknown") %}{{ dedicated_ip }}{% else %}{{ server_ip }}{% endif %}' class="form-control" readonly="">
<div id="smtpport" class="form-text">SMTP Port: <b>465</b></div>
</div>
</div>
IMAP and SMTP require authentication.
</form>
</div></div></div>
<div class="col-md-6 col-xl-6"><div class="card card-one"><div class="card-header"><h6 class="card-title">{{ _("Non-SSL Settings (NOT Recommended)
") }}</h6></div><div class="card-body">
<form>
<div class="form-group">
<label for="readOnlyFieldReal" class="form-label">Username:</label>
<div class="form-field">
<input type="text" value="{{address}}" class="form-control" readonly="">
</div>
</div>
<div class="form-group">
<label for="readOnlyFieldReal" class="form-label">Password:</label>
<div class="form-field">
<i>Use the email accounts password.</i>
</div>
</div>
<div class="form-group">
<label for="readOnlyFieldReal" class="form-label">Incoming Server:</label>
<div class="form-field">
<input type="text" value='{% if dedicated_ip != _("Unknown") %}{{ dedicated_ip }}{% else %}{{ server_ip }}{% endif %}' class="form-control" readonly="">
<div id="nonsslimapport" class="form-text">IMAP Port: <b>143</b></div>
</div>
</div>
<div class="form-group">
<label for="readOnlyFieldReal" class="form-label">Outgoing Server:</label>
<div class="form-field">
<input type="text" value='{% if dedicated_ip != _("Unknown") %}{{ dedicated_ip }}{% else %}{{ server_ip }}{% endif %}' class="form-control" readonly="">
<div id="nonsslsmtpport" class="form-text">SMTP Port: <b>587</b></div>
</div>
</div>
IMAP and SMTP require authentication.
</form>
</div></div></div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<footer class="main-footer btn-toolbar" role="toolbar">
<div class="btn-group" role="group" aria-label="Back">
<a href="/emails" class="btn btn-transparent" type="button" aria-expanded="false"><i class="bi bi-arrow-left-short"></i><span class="desktop-only"> Go Back</span></a>
</div>
<div class="ms-auto" role="group" aria-label="{{ _('Delete') }}">
<button id="scanButton" class="btn btn-danger" type="button"><i class="bi bi-trash3"></i><span class="desktop-only"> Delete</span></button>
</div>
</footer>
{% endblock %}

View File

@@ -0,0 +1 @@
<!DOCTYPE html><html><head><meta charset="utf-8"><title>OpenPanel</title><style media="screen">@keyframes fa-author-colors{0%{transform:rotateX(17deg) rotateY(9deg)}10%{transform:rotateX(27deg) rotateY(0)}30%{transform:rotateX(37deg) rotateY(19deg)}50%{transform:rotateX(-57deg) rotateY(9deg)}70%{transform:rotateX(20deg) rotateY(-10deg)}90%{transform:rotateX(17deg) rotateY(9deg)}100%{transform:rotateX(-32deg) rotateY(19deg)}}body{overflow:hidden;background:#1e1f1f;margin:0;color:#fff;font-size:14px;font-family:-apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"}p{font-size:15px;line-height:22px;margin:0}.mCont{text-align:center;perspective:320px}h1{font-weight:700;font-size:10em;margin:10% 0 30px;font-family:Monaco,"Andale Mono","Lucida Console","Bitstream Vera Sans Mono","Courier New",Courier,monospace;transform:rotateX(17deg) rotateY(9deg);transform-style:preserve-3d;animation:fa-author-colors 50s infinite}p span{background:rgba(255,255,255,.05);padding:15px 30px;border-radius:10px}</style></head><body><div class="mCont"><h1>404</h1><p><span>{{ _('That page does not exist') }}</span></p></div></body></html>

View File

@@ -0,0 +1 @@
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>{{ _('500 Error Page') }}</title><link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"></head><body><div class="d-flex align-items-center justify-content-center vh-100"><div class="text-center"><h1 class="display-1 fw-bold">500</h1><p class="fs-3"><span class="text-danger">{{ _('Server') }}</span>{{ _('Error') }}</p><p class="lead">{{ _('Click on the button bellow to reload. If the issue persists please contact support.') }}</p><a href="" onclick="window.location.reload()" class="btn btn-primary">{{ _('Refresh page') }}</a></div></div></body>

View File

@@ -0,0 +1,540 @@
{% extends 'base.html' %}
{% block content %}
<script type="module" src="https://cdn.jsdelivr.net/gh/lekoala/formidable-elements/dist/flatpickr-input.min.js"></script>
<div class="row g-3">
<div class="col">
<div class="card card-one">
<div class="card-header">
<h6 class="card-title">{{ _('Backup Storage') }}</h6>
<nav class="nav nav-icon nav-icon-sm ms-auto">
<a href="" class="nav-link"><i class="ri-refresh-line"></i></a>
<a href="" class="nav-link"><i class="ri-more-2-fill"></i></a>
</nav>
</div>
<div class="card-body">
<div class="row mb-0">
<div class="col">
<div class="card card-tile">
<div class="card-badge bg-primary">
<i class="bi bi-info"></i>
</div>
<div class="card-body">
<div class="card-title">{{ _('Queue') }}</div>
<div class="card-text">
<div class="fs-1 mt-1">0</div>
</div>
</div>
</div>
</div>
<div class="col">
<div class="card card-tile">
<div class="card-badge bg-danger">
<i class="bi bi-clock-history"></i>
</div>
<div class="card-body">
<div class="card-title">{{ _('Total Backups') }}</div>
<div class="card-text">
<div class="fs-1 mt-1">{{ num_backups if num_backups else '0' }}</div>
</div>
</div>
</div>
</div>
<div class="col">
<div class="card card-tile">
<div class="card-badge bg-primary">
<i class="bi bi-hdd-fill"></i>
</div>
<div class="card-body">
<div class="card-title">{{ _('Total Account usage (GB)') }}</div>
<div class="card-text">
<div class="fs-1 mt-1">14 GB</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="">
<div class="card card-one">
<div class="card-header">
<h6 class="card-title">{{ _('Backups') }}</h6>
<nav class="nav nav-icon nav-icon-sm ms-auto">
<a href="" class="nav-link"><i class="ri-refresh-line"></i></a>
<a href="" class="nav-link"><i class="ri-more-2-fill"></i></a>
</nav>
</div>
<div class="card-body">
<div class="row mb-4">
<table class="table table-bordered table-hover" id="backupTable">
<thead>
<tr>
<th>Created</th>
<th>End Time</th>
<th>Duration</th>
<th>Status</th>
<th>Contains</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<!-- Rows will be dynamically added here -->
</tbody>
</table>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
// Fetch backup data using AJAX
fetch('/backups/info/')
.then(response => response.json())
.then(data => {
if (data.backups) {
populateTable(data.backups);
} else {
console.error('No backup data found');
}
})
.catch(error => {
console.error('Error fetching data:', error);
});
});
document.addEventListener('DOMContentLoaded', function () {
// Fetch backup data using AJAX
fetch('/backups/info/')
.then(response => response.json())
.then(data => {
if (data.backups) {
populateTable(data.backups);
} else {
showError('No backup data found');
}
})
.catch(error => {
showError('Error fetching data: ' + error.message);
});
});
function populateTable(backups) {
const tableBody = document.querySelector('#backupTable tbody');
tableBody.innerHTML = ''; // Clear the table body
backups.forEach(backup => {
const backupDate = backup.backup_date; // Use the raw backup date
const content = backup.content;
// Create a new row
const row = document.createElement('tr');
// Create table cells for each property
//row.appendChild(createTableCell(backupDate));
//row.appendChild(createTableCell(content.backup_job_id || '-'));
//row.appendChild(createTableCell(content.destination_id || '-'));
//row.appendChild(createTableCell(content.destination_directory || '-'));
row.appendChild(createTableCell(content.start_time || '-')); // Use raw start time
row.appendChild(createTableCell(content.end_time || '-')); // Use raw end time
row.appendChild(createTableCell(content.total_exec_time || '-'));
row.appendChild(createStatusCell(content.status || 'Unknown')); // Create status cell
// Create a single cell for all "contains" icons
const contains = content.contains ? content.contains.split(',') : [];
row.appendChild(createContainsIconsCell(contains));
// Create a cell for the buttons
const buttonCell = document.createElement('td');
// Create Download button
const downloadButton = document.createElement('button');
downloadButton.textContent = 'Download';
downloadButton.className = 'btn btn-secondary';
downloadButton.onclick = () => {
// todo
console.log(`Downloading backup:
Backup Job ID: ${content.backup_job_id},
Destination ID: ${content.destination_id},
Destination Directory: ${content.destination_directory},
Backup Date: ${backupDate}`);
// window.location.href = `download_url/${content.backup_job_id}`;
};
// Create Restore button
const restoreButton = document.createElement('button');
restoreButton.textContent = 'Restore';
restoreButton.className = 'btn btn-primary';
restoreButton.onclick = () => {
// todo
console.log(`Restoring backup:
Backup Job ID: ${content.backup_job_id},
Destination ID: ${content.destination_id},
Destination Directory: ${content.destination_directory},
Backup Date: ${backupDate}`);
// Example restore logic
};
// Append buttons to the button cell
buttonCell.appendChild(downloadButton);
buttonCell.appendChild(restoreButton);
// Append the button cell to the row
row.appendChild(buttonCell);
// Append the row to the table body
tableBody.appendChild(row);
});
}
// Helper function to create a table cell
function createTableCell(text) {
const cell = document.createElement('td');
cell.textContent = text;
return cell;
}
// Helper function to create a status cell with an icon
function createStatusCell(status) {
const cell = document.createElement('td');
const icon = document.createElement('i');
let iconClass = '';
let statusText = status;
switch (status) {
case 'Completed':
iconClass = 'fa-check-circle'; // Check icon
break;
case 'Partial':
iconClass = 'fa-exclamation-circle'; // Warning icon
break;
case 'In Progress':
iconClass = 'fa-spinner fa-spin'; // Spinner icon
break;
default:
iconClass = 'fa-question-circle'; // Question icon
statusText = 'Unknown'; // Update the text for unknown status
}
icon.classList.add('fas', iconClass, 'me-2'); // Add icon class and margin
cell.appendChild(icon);
cell.appendChild(document.createTextNode(statusText)); // Append the status text
return cell;
}
// Helper function to create a table cell with multiple icons
function createContainsIconsCell(containsArray) {
const cell = document.createElement('td');
// Map each contains item to an icon and tooltip
const iconMap = {
'FILES': 'fa-file',
'ENTRYPOINT': 'fa-play',
'WEBSERVER_CONF': 'fa-server',
'MYSQL_CONF': 'fa-database',
'TIMEZONE': 'fa-clock',
'PHP_VERSIONS': 'fa-code',
'CRONTAB': 'fa-calendar-alt',
'MYSQL_DATA': 'fa-database',
'USER_DATA': 'fa-users',
'CORE_USERS': 'fa-user-shield',
'STATS_USERS': 'fa-chart-bar',
'APACHE_SSL_CONF': 'fa-lock',
'DOMAIN_ACCESS_REPORTS': 'fa-file-alt',
'SSH_PASS': 'fa-key',
'IMAGE': 'fa-image'
};
containsArray.forEach(item => {
if (iconMap[item]) {
const icon = document.createElement('i');
icon.classList.add('fas', iconMap[item], 'me-2'); // Add icon class and margin
icon.setAttribute('title', item); // Set the tooltip text
icon.setAttribute('data-bs-toggle', 'tooltip'); // Enable tooltip
cell.appendChild(icon);
}
});
if (cell.childNodes.length === 0) {
cell.textContent = '-'; // If no icons were added, display a dash
}
return cell;
}
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/js/all.min.js"></script> <!-- Font Awesome Icons -->
</div>
<div class="col-auto">
<div class="card card-one">
<div class="card-header">
<h6 class="card-title">{{ _('Restore') }}</h6>
<nav class="nav nav-icon nav-icon-sm ms-auto">
<a href="" class="nav-link"><i class="ri-refresh-line"></i></a>
<a href="" class="nav-link"><i class="ri-more-2-fill"></i></a>
</nav>
</div>
<div class="card-body row row-cols-4">
<a href="#" class="col" data-bs-toggle="modal" data-bs-target="#filesModal">
<div class="card p-3 d-flex flex-row mb-2">
<div class="card-icon avatar avatar-thumb bg-primary"><i class="bi bi-archive"></i></div>
<div class="ms-3">
<h4 class="card-value mb-1">{{ _('Restore Files') }}</h4>
<p class="fs-xs text-secondary mb-0 lh-4">{{ _('Restore your files from backup') }}</p>
</div>
</div>
</a>
<a href="#" class="col" data-bs-toggle="modal" data-bs-target="#databasesModal">
<div class="card p-3 d-flex flex-row">
<div class="card-icon avatar avatar-thumb bg-primary"><i class="bi bi-database"></i></div>
<div class="ms-3">
<h4 class="card-value mb-1">{{ _('Databases') }}</h4><p class="fs-xs text-secondary mb-0 lh-4">{{ _('Restore MySQL databases and their tables') }}</p>
</div>
</div>
</a>
<a href="#" class="col" data-bs-toggle="modal" data-bs-target="#databaseUsersModal">
<div class="card p-3 d-flex flex-row">
<div class="card-icon avatar avatar-thumb bg-primary"><i class="bi bi-database-lock"></i></div>
<div class="ms-3">
<h4 class="card-value mb-1">{{ _('Database Users') }}</h4><p class="fs-xs text-secondary mb-0 lh-4">{{ _('Restore MySQL database users and privileges') }}</p>
</div>
</div>
</a>
<a href="#" class="col" data-bs-toggle="modal" data-bs-target="#cronsModal">
<div class="card p-3 d-flex flex-row">
<div class="card-icon avatar avatar-thumb bg-primary"><i class="bi bi-calendar2-week"></i></div>
<div class="ms-3">
<h4 class="card-value mb-1">{{ _('Cron Jobs') }}</h4><p class="fs-xs text-secondary mb-0 lh-4">{{ _('Restore cronjobs from backup') }}</p>
</div>
</div>
</a>
<a href="#" class="col" data-bs-toggle="modal" data-bs-target="#dnsModal">
<div class="card p-3 d-flex flex-row">
<div class="card-icon avatar avatar-thumb bg-primary"><i class="bi bi-geo-alt"></i></div>
<div class="ms-3">
<h4 class="card-value mb-1">{{ _('DNS Zones') }}</h4><p class="fs-xs text-secondary mb-0 lh-4">{{ _('Restore DNS zones and records') }}</p>
</div>
</div>
</a>
<a href="#" class="col" data-bs-toggle="modal" data-bs-target="#domainsModal">
<div class="card p-3 d-flex flex-row">
<div class="card-icon avatar avatar-thumb bg-primary"><i class="bi bi-globe"></i></div>
<div class="ms-3">
<h4 class="card-value mb-1">{{ _('Domains') }}</h4><p class="fs-xs text-secondary mb-0 lh-4">{{ _('Restore domain names') }}</p>
</div>
</div>
</a>
<a href="#" class="col" data-bs-toggle="modal" data-bs-target="#sslModal">
<div class="card p-3 d-flex flex-row">
<div class="card-icon avatar avatar-thumb bg-primary"><i class="bi bi-lock"></i></div>
<div class="ms-3">
<h4 class="card-value mb-1">{{ _('Certificates') }}</h4><p class="fs-xs text-secondary mb-0 lh-4">{{ _('Restore SSL certificates') }}</p>
</div>
</div>
</a>
<a href="#" class="col" data-bs-toggle="modal" data-bs-target="#emailModal">
<div class="card p-3 d-flex flex-row">
<div class="card-icon avatar avatar-thumb bg-primary"><i class="bi bi-envelope"></i></div>
<div class="ms-3">
<h4 class="card-value mb-1">{{ _('Emails') }}</h4><p class="fs-xs text-secondary mb-0 lh-4">{{ _('Restore email accounts') }}</p>
</div>
</div>
</a>
<a href="#" class="col" data-bs-toggle="modal" data-bs-target="#emailModal">
<div class="card p-3 d-flex flex-row">
<div class="card-icon avatar avatar-thumb bg-primary"><i class="bi bi-folder-symlink"></i></div>
<div class="ms-3">
<h4 class="card-value mb-1">{{ _('FTP') }}</h4><p class="fs-xs text-secondary mb-0 lh-4">{{ _('Restore FTP accounts') }}</p>
</div>
</div>
</a>
<a href="#" class="col" data-bs-toggle="modal" data-bs-target="#confModal">
<div class="card p-3 d-flex flex-row">
<div class="card-icon avatar avatar-thumb bg-primary"><i class="bi bi-file"></i></div>
<div class="ms-3">
<h4 class="card-value mb-1">{{ _('Configuration') }}</h4><p class="fs-xs text-secondary mb-0 lh-4">{{ _('Restore services configuration files') }}</p>
</div>
</div>
</a>
</div>
</div>
</div>
<div class="col-md-6 col-xl-6">
<div class="card card-one">
<div class="card-header">
<h6 class="card-title">{{ _('Backup Logs') }}</h6>
<nav class="nav nav-icon nav-icon-sm ms-auto">
<a href="" class="nav-link"><i class="ri-refresh-line"></i></a>
<a href="" class="nav-link"><i class="ri-more-2-fill"></i></a>
</nav>
</div>
<div class="card-body p-0" style="overflow:auto; height:300px">
<ul id="backup-list" class="list-group">
</ul>
</div>
<!--div class="card-footer d-flex justify-content-center">
<a href="" class="fs-sm">{{ _('View Full Backup Log') }}</a>
</div-->
</div>
</div>
<div class="col-md-6 col-xl-6">
<div class="card card-one">
<div class="card-header">
<h6 class="card-title">{{ _('Restore History') }}</h6>
<nav class="nav nav-icon nav-icon-sm ms-auto">
<a href="" class="nav-link"><i class="ri-refresh-line"></i></a>
<a href="" class="nav-link"><i class="ri-more-2-fill"></i></a>
</nav>
</div>
<div class="card-body p-0" style="overflow:auto; height:300px">
<ul id="restore-list" class="list-group">
</ul>
</div>
<!--div class="card-footer d-flex justify-content-center">
<a href="" class="fs-sm">{{ _('View Full Restore Log') }}</a>
</div-->
</div>
</div>
</div>
<div class="modal fade" id="filesModal" tabindex="-1" role="dialog" aria-labelledby="filesModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="filesModalLabel">{{ _('Restore Files') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="restoreForm">
<div class="form-group">
<label for="backupDate">Select a backup date:</label>
<select class="form-select" id="backupDate" name="backupdate">
{% if backup_dates %}
{% for date in backup_dates %}
<option value="{{ date }}">{{ date }}</option>
{% endfor %}
{% else %}
<p>{{ _('No backups available.') }}</p>
{% endif %}
</select>
</div>
</form>
<div class="mt-3" id="restoreStatus"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ _('Cancel') }}</button>
<button type="button" class="btn btn-primary" id="restoreButton">{{ _('Restore') }}</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="databasesModal" tabindex="-1" role="dialog" aria-labelledby="databasesModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="databasesModalLabel">{{ _('Restore MySQL Database') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="restoreForm">
<div class="form-group">
<label for="backupDate">{{ _('Select a backup date:') }}</label>
<select class="form-select" id="backupDbDate" name="backup_date">
{% if backup_dates %}
{% for date in backup_dates %}
<option value="{{ date }}">{{ date }}</option>
{% endfor %}
{% else %}
<p>{{ _('No backups available.') }}</p>
{% endif %}
</select>
</div>
<div class="form-group">
<label for="sqlFile">{{ _('Select a database:') }}</label>
<select class="form-select" id="sqlFiles" name="sql_file">
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ _('Cancel') }}</button>
<button type="button" class="btn btn-primary" id="restoreDbButton">{{ _('Restore') }}</button>
</div>
</div>
</div>
</div>
<script src="/static/js/backup.js?v=1.0.0"></script>
{% endblock %}

View File

@@ -0,0 +1,255 @@
{% extends 'base.html' %}
{% block content %}
<div class="row g-3">
<p>{{_('Disk usage is the amount of space that is used by the content of your sites, this includes databases, files, videos, images, emails and pages.')}}</p>
<div class="col-xl-7">
<div class="card card-one">
<div class="card-header">
<h6 class="card-title">{{_('Disk usage per directory')}}</h6>
<nav class="nav nav-icon nav-icon-sm ms-auto">
<a href="#" onclick='location.reload(true); return false;' class="nav-link"><i class="bi bi-arrow-clockwise"></i></a>
</nav>
</div><!-- card-header -->
<div class="card-body">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<canvas id="folderChart" width="400" height="200"></canvas>
</div><!-- card-body -->
</div><!-- card -->
</div><!-- col -->
<div class="col-sm-12 col-xl-5">
<div class="card card-one">
<div class="card-header">
<h6 class="card-title">{{_('Browse Directories')}}</h6>
<nav class="nav nav-icon nav-icon-sm ms-auto">
<a href="/disk-usage" class="nav-link"><i class="bi bi-house-fill"></i></a>
</nav>
</div><!-- card-header -->
<div class="card-body">
<table id="folders_to_navigate" class="table table-hover table-striped">
<thead>
<tr>
<th>Directory</th>
<th>Size</th>
</tr>
</thead>
<tbody>
{% if request.path != '/disk-usage/' %}
<tr>
<td><a href="#" id="goUp"><i class="bi bi-arrow-90deg-up"></i> {{_('Up One Level')}}</a></td>
<td></td>
</tr>
{% endif %}
{% for line in total_du_output.split('\n') %}
{% if line.strip() %}
<tr>
{% set parts = line.split() %}
{% set count = parts[0] %}
{% set directory = parts[1:]|join(' ') %}
<td class="file-name"><a href="#" onclick="openDirectory('{{ directory }}'); return false;"><i style="color: orange;" class="ri-folder-2-fill"></i> {{ directory }}</a>
</td>
<td>{{ count }}</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div><!-- card-body -->
</div><!-- card -->
</div><!-- col -->
</div>
<script>
async function openDirectory(directory) {
// Display a toast message indicating that disk usage is being calculated for the specified directory
const toastMessage = `{{_('Calculating disk usage for ')}}` + directory + `..`;
const toast = toaster({
body: toastMessage,
className: `border-0 text-white bg-primary`,
});
// Construct the new URL by appending the directory to the current URL
var currentUrl = window.location.href;
var separator = currentUrl.endsWith('/') ? '' : '/';
var newUrl = currentUrl + separator + directory;
try {
// Send a GET request to the server using the fetch API
const response = await fetch(newUrl);
// Get the response HTML content
const resultHtml = await response.text();
// Parse the HTML string to extract the content of the specific element
const parser = new DOMParser();
const doc = parser.parseFromString(resultHtml, 'text/html');
const mainScopeContent = doc.getElementById("main-scope")?.innerHTML;
// Replace the content of the element with the ID "main-scope"
const mainScopeElement = document.getElementById("main-scope");
if (mainScopeElement) {
mainScopeElement.innerHTML = mainScopeContent || '';
}
//add to url wothout reloading!
history.pushState({}, '', newUrl);
// Initialize the chart after updating the content and URL
initializeChart();
} catch (error) {
// Handle errors if the fetch request fails
console.error('Error fetching data:', error);
}
}
</script>
<script>
// Function to extract data from the table
function extractDataFromTable() {
const data = [];
const tableRows = document.querySelectorAll("#folders_to_navigate tbody tr");
tableRows.forEach((row) => {
const columns = row.querySelectorAll("td");
if (columns.length === 2) {
const directory = columns[0].textContent.trim();
const disk = parseHumanReadableSize(columns[1].textContent);
// Exclude rows with "Up One Level"
if (directory !== "Up One Level") {
data.push({ directory, disk });
}
}
});
return data;
}
// Function to parse human-readable size strings into MB
function parseHumanReadableSize(sizeStr) {
const sizeRegex = /^(\d+(\.\d+)?)\s*([BKMGT])?B?$/;
const match = sizeStr.match(sizeRegex);
if (!match) {
// Invalid format, return as is
return sizeStr;
}
const size = parseFloat(match[1]);
const unit = match[3];
switch (unit) {
case 'B':
return size / 1024 / 1024; // Convert to MB
case 'K':
return size / 1024; // Convert to MB
case 'M':
return size; // Already in MB
case 'G':
return size * 1024; // Convert to MB
case 'T':
return size * 1024 * 1024; // Convert to MB
default:
return size; // Unknown unit, return as is
}
}
// Function to create a chart
function createChart(data) {
const labels = data.map((entry) => entry.directory);
const values = data.map((entry) => entry.disk);
const ctx = document.getElementById("folderChart").getContext("2d");
const chart = new Chart(ctx, {
type: "bar",
data: {
labels: labels,
datasets: [
{
label: "Disk Usage (MB)",
data: values, // The values are already in MB
backgroundColor: "#506fd9",
},
],
},
});
// console.log(data);
}
// Function to initialize the chart on page load
function initializeChart() {
const tableData = extractDataFromTable();
createChart(tableData);
RefreshOnBackLink();
}
// go back link
function RefreshOnBackLink() {
const goUpElement = document.getElementById('goUp');
if (goUpElement) {
goUpElement.addEventListener('click', async function() {
const toastMessage = `{{_('Calculating disk usage for parent directory..')}}`;
const toast = toaster({
body: toastMessage,
className: `border-0 text-white bg-primary`,
});
var currentUrl = window.location.href;
var parts = currentUrl.split('/');
parts.pop();
var newUrl = parts.join('/');
try {
const response = await fetch(newUrl);
if (!response.ok) {
throw new Error(`Failed to fetch ${newUrl}`);
}
const resultHtml = await response.text();
// Parse the HTML string to extract the content of the specific element
const parser = new DOMParser();
const doc = parser.parseFromString(resultHtml, 'text/html');
const mainScopeContent = doc.getElementById("main-scope")?.innerHTML;
// Replace the content of the element with the ID "main-scope"
const mainScopeElement = document.getElementById("main-scope");
if (mainScopeElement) {
mainScopeElement.innerHTML = mainScopeContent || '';
}
// Update the browser history
history.pushState({}, '', newUrl);
// Initialize the chart after updating the content and URL
initializeChart();
} catch (error) {
console.error('Error fetching data:', error);
}
});
}
}
</script>
<script>
initializeChart();
</script>
{% endblock %}

View File

@@ -0,0 +1,179 @@
{% extends 'base.html' %}
{% block content %}
{% if file_content == '404_file_not_exsist_error' %}
<div class="row text-center">
<img src="/static/images/not-found.png" class="text-center" style="max-width:30%; margin-left: auto; margin-right: auto;">
<div class="fs-1 fw-bolder text-dark mb-4">
{{_('The specified file does not exist.')}}
</div>
<div class="fs-6">/home/{{ current_username }}/{{ file_path }}</div>
</div>
{% else %}
<style>
html, body {
height: 100%; /* Ensure html and body occupy full height */
margin: 0; /* Remove default margin */
}
.container-fluid {
padding:0;
}
.editor-container {
min-height: 90vh; /* Set a minimum height */
height: 100%; /* Occupy full height */
}
#editor-container.fullscreen {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 9999;
background-color: #1e1e1e; /* Monaco Dark Background */
}
.editor-header {
display: flex;
padding: 0px 10px;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #8080801c;
}
#editor-container.fullscreen h3 { display: none; }
#fullscreenButton { margin-left: 10px; }
</style>
<!-- Include Monaco Editor CSS and JS -->
<link rel="stylesheet" href="/static/monaco/editor.main.css">
<script src="/static/monaco/loader.js"></script>
<form method="post">
<input type="hidden" name="editor_content" id="editor_content"> <!-- Hidden input to store editor content -->
<div id="editor-container">
<div class="editor-header">
<h6 class="m-0">/home/{{ current_username }}/{{ file_path }}</h6>
<div>
<button type="submit" class="btn btn-primary" id="editorcontentvalue">{{_('Save')}}</button>
<button type="button" class="btn btn-outline" id="fullscreenButton"><i class="bi bi-arrows-fullscreen"></i></button>
</div>
</div>
<div class="editor-container" id="editor"></div>
</div>
</form>
<script>
require.config({ paths: { 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.36.1/min/vs' }});
require(['vs/editor/editor.main'], function() {
const filePath = '{{ file_path }}';
// Safely inject file content using JSON encoding
const fileContent = {{ file_content|tojson|safe }};
// Function to determine the language based on the file extension
function getLanguageFromFileName(fileName) {
const extension = fileName.split('.').pop().toLowerCase();
const languageMap = {
'py': 'python',
'js': 'javascript',
'html': 'html',
'css': 'css',
'scss': 'scss',
'java': 'java',
'c': 'c',
'cpp': 'cpp',
'php': 'php',
'sql': 'sql',
'sh': 'shell',
'md': 'markdown',
'ini': 'ini',
'gitconfig': 'ini',
'less': 'less',
'mysql': 'mysql',
'mdx': 'mdx',
'perl': 'perl',
'pqsql': 'pqsql',
'redis': 'redis',
'rust': 'rust',
'swift': 'swift',
'ruby': 'ruby',
'twig': 'twig',
'typescript': 'typescript',
'xml': 'xml',
'yaml': 'yaml',
'json': 'json',
};
return languageMap[extension] || 'plaintext'; // Default
}
const language = getLanguageFromFileName(filePath);
const editor = monaco.editor.create(document.getElementById('editor'), {
value: fileContent,
language: language,
theme: 'vs-dark', // Set theme to Dark
automaticLayout: true,
lineNumbers: "on", // Enable line numbers
});
const fullscreenButton = document.getElementById('fullscreenButton');
const editorContainer = document.getElementById('editor-container');
let isFullscreen = false;
fullscreenButton.addEventListener('click', () => {
if (!isFullscreen) {
editorContainer.classList.add('fullscreen');
isFullscreen = true;
} else {
editorContainer.classList.remove('fullscreen');
isFullscreen = false;
}
editor.layout(); // Adjust editor layout when toggling fullscreen
});
// Update the hidden input with the editor content before form submission
const saveButton = document.getElementById('editorcontentvalue');
saveButton.addEventListener('click', () => {
// Set the value of the hidden input to the content of the editor
document.getElementById('editor_content').value = editor.getValue();
});
// Scroll to specific line based on URL fragment (e.g., #42 for line 42)
function scrollToLineFromURL() {
const hash = window.location.hash;
if (hash && hash.startsWith('#')) {
const lineNumber = parseInt(hash.substring(1));
if (!isNaN(lineNumber)) {
editor.revealLineInCenter(lineNumber); // Scroll to line
editor.setPosition({ lineNumber: lineNumber, column: 1 }); // Set cursor to start of the line
editor.focus();
}
}
}
// Call scrollToLineFromURL when the page loads
scrollToLineFromURL();
// Optionally, update the URL fragment as the user scrolls or moves the cursor
editor.onDidChangeCursorPosition(() => {
const position = editor.getPosition();
if (position) {
const newHash = `#${position.lineNumber}`;
if (window.location.hash !== newHash) {
history.replaceState(null, null, newHash); // Update the URL fragment without reloading
}
}
});
});
</script>
{% endif %}
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,101 @@
{% extends 'base.html' %}
{% block content %}
<p>{{ _('Fix and reset permissions for files and folders.') }}</p>
<div class="container">
<div class="col-auto">
<label class="directory-select-label" for="directory-select">{{ _('Choose a directory') }}</label><br>
<div class="input-group">
<input type="text" id="directory-select" class="form-control" list="directory-options">
<datalist id="directory-options">
{% for directory in directories %}
<option value="{{ directory }}">
{% endfor %}
</datalist>
<span class="input-group-btn">
<button id="start-scan-btn" class="btn btn-primary" tabindex="-1">{{ _('Fix Permissions') }}</button>
<!-- Fixing Spinner Button (Initially hidden) -->
<button id="scanning-btn" class="btn btn-primary" tabindex="-1" type="button" style="display: none;" disabled>
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
{{ _('Working...') }}
</button>
</span>
</div>
</div>
<!-- Fix Complete Message (Initially hidden) -->
<div id="scan-complete-message" class="alert alert-success mt-3 mb-3" style="display: none;">
{{ _('Permissions are fixed!') }}
</div>
</div>
<script>
// Function to show the scan complete message
function showScanCompleteMessage() {
document.getElementById('scan-complete-message').style.display = 'block';
}
// Function to initiate the scan when the "Start Scan" button is clicked
document.getElementById('start-scan-btn').addEventListener('click', function () {
// Hide the "Start Scan" button and show the scanning spinner button
document.getElementById('start-scan-btn').style.display = 'none';
document.getElementById('scanning-btn').style.display = 'inline-block';
// Get the selected directory from the dropdown
const selectedDirectory = document.getElementById('directory-select').value;
const toast = toaster({
body: 'Process started',
className: 'border-0 text-white bg-primary',
});
document.getElementById('scan-complete-message').style.display = 'none';
// Send the selected directory to the server for scanning
fetch(`/fix-permissions`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `directory=${encodeURIComponent(selectedDirectory)}`,
})
.then(response => {
if (!response.ok) {
throw new Error('{{ _("Network response was not ok") }}');
}
return response.text(); // Change to response.text() to read the response body
})
.then(data => {
// Process the response data if needed
console.log(data);
// Show scan complete message
showScanCompleteMessage();
})
.catch(error => {
console.error('Fixing permissions failed:', error);
// Display an error message
const toast = toaster({
body: '{{ _("Fixing permissions failed") }}',
className: 'border-0 text-white bg-error',
});
})
.finally(() => {
// Display a success message
const toast = toaster({
body: '{{ _("Complete") }}',
className: 'border-0 text-white bg-success',
});
// Hide the scanning spinner button and show the "Start Scan" button
document.getElementById('scanning-btn').style.display = 'none';
document.getElementById('start-scan-btn').style.display = 'block';
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,530 @@
{% extends 'base.html' %}
{% block content %}
<!-- "Add New Domain" form -->
<form action="/ftp/add" method="post" id="addFTPForm" style="display: none;">
<div class="col-md-12">
<div class="card mb-3" style="">
<div class="card-body">
<div class="row">
<div class="col-4">
<label for="domain_url">{{ _('Username:') }}</label>
<div class="input-group">
<input type="text" class="form-control" name="username" pattern="[A-Za-z0-9]+" id="new_ftp_username" placeholder="" required>
<div class="input-group-append">
<span class="input-group-text" data-toggle="tooltip" data-placement="bottom" title="{{ _('FTP username must end with Dot followed by your OpenPanel username') }} (example.{{ current_username }})." name="path">.{{ current_username }}</span>
</div>
</div>
</div>
<div class="col-4">
<label for="domain_name">{{ _('Pasword:') }}</label>
<div class="input-group">
<input type="password" value="" id="admin_password" name="password" class="form-control" required
minlength="8"
pattern="^(?=.*?[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@.\-_=\+]).{8,}$"
title="Password must be at least 8 characters long, contain at least one uppercase letter, one lowercase letter, one digit, and at least one special character from @.-_=+">
<div class="input-group-append">
<button class="btn btn-outline-success" type="button" id="generatePassword">
{{ _("Generate") }}
</button>
<button class="btn btn-outline-secondary" type="button" id="togglePassword">
<i class="bi bi-eye"></i>
</button>
</div>
</div>
</div>
<div class="col-4">
<label for="domain_name">{{ _('Path (folder):') }}</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text" data-toggle="tooltip" data-placement="bottom" title="{{ _('Path must be under your home directory') }} /home/{{ current_username }}/">/home/{{ current_username }}/</span>
</div>
<input type="text" name="path" id="new_user_path" class="form-control">
</div>
</div>
</div>
<p class="lead mt-2">
<button type="submit" class="btn btn-lg btn-primary">{{ _('Add Account') }}</button>
</p>
</div>
</div>
</div>
</form>
<style>
thead {
border: 1px solid rgb(90 86 86 / 11%);
}
th {
text-transform: uppercase;
font-weight: 400;
}
[data-bs-theme=light] .domain_link {
color: black;
}
.domain_link {
border-bottom: 1px dashed #999;
text-decoration: none;
}
</style>
<p>{{ _('Use your FTP account with an FTP client such as ') }}<a data-e2e="link" data-component="link" tabindex="0" href="https://filezilla-project.org" role="link" target="_blank">FileZilla</a> {{ _('to transfer files to and from your website.') }}</p>
<button type="button" id="hiddenModalTrigger" style="display: none;" data-toggle="modal" data-target="#addFtpAccountModal"></button>
<div class="table-responsive">
<table class="table table-hover" id="ftp-accounts-table">
<thead>
<tr>
<th>{{ _('Username (Login)') }}</th>
<th data-toggle="tooltip" data-placement="bottom" title="{{ _('FTP sser has permissions to access only files and folders under this path.') }}" class="desktop-only">{{ _('Path') }}</th>
<!-- <th>Usage/Quota</th> -->
<th>{{ _('Actions') }}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<script>
$(document).ready(function() {
// Function to fetch FTP accounts data via AJAX
function fetchFtpAccounts() {
$.ajax({
url: '/ftp/list',
method: 'GET',
dataType: 'json',
success: function(data) {
// Clear existing table content
$('#ftp-accounts-table tbody').empty();
// Check if data contains 'ftp_accounts' key
if ('ftp_accounts' in data) {
// Iterate through the FTP accounts and add rows
$.each(data.ftp_accounts, function(index, account) {
// Extract data from the account object
var username = account.username;
var path = account.path;
let strippedPath = path.replace('/home/{{ current_username }}/', '');
var usageQuota = account.usageQuota;
// Create a row with buttons for each FTP account
var row = '<tr class="domain_row">' +
'<td><i class="bi bi-person-fill"></i> <span id="databaseUsername">' + username + '</span><small class="mobile-only"><br><a href="/files/' + strippedPath + '"><i style="color: orange;" class="bi bi-folder-fill"></i> <span class="domain_link">/files/' + path + '</span></a></small></td>' +
'<td class="desktop-only"><a href="/files/' + strippedPath + '"><i style="color: orange;" class="bi bi-folder-fill"></i> <span class="domain_link" id="databasePath">' + path + '</span></a><button class="btn btn-transparent" style="float: right" type="button" data-bs-toggle="modal" data-bs-target="#changePathModal""><i class="bi bi-pencil-fill"></i><span class="desktop-only"> Change Path</span></button>' + '</td>' +
//'<td>' + usageQuota + '</td>' +
'<td>' +
'<div class="d-flex gap-2 mt-3 mt-md-0">' +
'<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#changePasswordModal"><i class="bi bi-key-fill"></i> Change Password</button>' +
'<form method="POST" action="{{ url_for("del_ftp_account") }}">' +
'<input type="hidden" name="username" value="' + username + '">' +
'<button class="btn btn-danger" type="button" onclick="confirmDelete(this);"><i class="bi bi-trash3"></i> Delete</button>' +
'</form>' +
//'<button class="btn btn-primary">Change Quota</button>' +
//'<button class="btn btn-primary" type="button">Change Path</button>' +
'</div>' +
'</td>' +
'</tr>';
$('#ftp-accounts-table tbody').append(row);
});
} else {
// Handle the case where the JSON response is unexpected
var errorMsg = '<tr><td colspan="4">No FTP accounts.</td></tr>';
$('#ftp-accounts-table tbody').append(errorMsg);
}
},
error: function(error) {
// Handle the AJAX error
console.error('Error fetching FTP accounts:', error);
var errorMsg = '<tr><td colspan="4">Error fetching FTP accounts data.</td></tr>';
$('#ftp-accounts-table tbody').append(errorMsg);
}
});
}
// load accounts
fetchFtpAccounts();
// add search
initializeFTPManagement();
// change password modal
changePassModal();
// change path modal
changePathModal();
});
</script>
</div>
<!-- Change Password Modal -->
<div class="modal fade" id="changePasswordModal" tabindex="-1" aria-labelledby="changePasswordModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="changePasswordModalLabel">{{_('Change Password')}}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label=" {{ _('Close') }}"></button>
</div>
<div class="modal-body">
<!-- Change Password Form Content -->
<form id="changePasswordForm" method="POST" action="{{ url_for('chn_ftp_pass') }}">
<div class="form-group">
<label for="username">{{_('FTP user')}}</label>
<input type="text" name="username" class="form-control" placeholder=" {{ _('User') }}" required disabled>
<input type="text" name="username" class="form-control" placeholder=" {{ _('User') }}" required hidden>
</div>
<div class="form-group">
<label for="new_password">{{_('New Password')}}</label>
<div class="input-group">
<input type="password" value="" id="new_password" name="new_password" class="form-control" required
minlength="8"
pattern="^(?=.*?[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@.\-_=\+]).{8,}$"
title="Password must be at least 8 characters long, contain at least one uppercase letter, one lowercase letter, one digit, and at least one special character from @.-_=+">
<div class="input-group-append">
<button class="btn btn-outline-success" type="button" id="generatePasswordinModal">
{{ _("Generate") }}
</button>
<button class="btn btn-outline-secondary" type="button" id="togglePasswordinModal">
<i class="bi bi-eye"></i>
</button>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="submit" form="changePasswordForm" class="btn btn-primary">{{_('Change Password')}}</button>
</div>
</div>
</div>
</div>
<!-- Change Path Modal -->
<div class="modal fade" id="changePathModal" tabindex="-1" aria-labelledby="changePathModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="changePathModalLabel">{{_('Change Path')}}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label=" {{ _('Close') }}"></button>
</div>
<div class="modal-body">
<!-- Change changePathModal Form Content -->
<form id="changePathForm" method="POST" action="{{ url_for('chn_ftp_path') }}">
<div class="form-group">
<label for="username">{{_('FTP user')}}</label>
<input type="text" name="username" class="form-control" placeholder=" {{ _('User') }}" required disabled>
<input type="text" name="username" class="form-control" placeholder=" {{ _('User') }}" required hidden>
</div>
<div class="form-group">
<label for="path">{{_('Current Path')}}</label>
<input type="text" name="current_path" class="form-control" value="" required disabled>
</div>
<div class="form-group">
<label for="new_path">{{_('New Path')}}</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text" data-toggle="tooltip" data-placement="bottom" title="{{ _('Path must be under your home directory') }} /home/{{ current_username }}/">/home/{{ current_username }}/</span>
</div>
<input type="text" name="new_path" class="form-control">
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="submit" form="changePathForm" class="btn btn-primary">{{_('Change Path')}}</button>
</div>
</div>
</div>
</div>
</section>
<footer class="main-footer btn-toolbar" role="toolbar">
<div class="btn-group" role="group" aria-label="{{ _('Searh') }}">
<input type="text" class="d-none form-control" placeholder="{{ _('Search accounts') }}" id="searchFTPInput">
{{ _('FTP server') }}:&nbsp; <b>{% if dedicated_ip != _("Unknown") %}{{ dedicated_ip }}{% else %}{{ server_ip }}{% endif %}</b>
</div>
<div class="col-4">
{{ _('FTP port') }}: <b>21</b>
</div>
<div class="ms-auto" role="group" aria-label="{{ _('Actions') }}">
<button type="button" class="btn btn-primary d-flex align-items-center gap-2" id="showAddFTPFormBtn"><i class="bi bi-plus-lg"></i> <span class="desktop-only">{{ _('Add Account') }}</span></button>
</div>
</footer>
<script>
// CREATE USER
const addFTPForm = document.getElementById("addFTPForm");
const showAddFTPFormBtn = document.getElementById("showAddFTPFormBtn"); // Corrected ID
// Ensure the addFTPForm exists
if (addFTPForm && showAddFTPFormBtn) {
showAddFTPFormBtn.addEventListener("click", function() {
// Toggle display of addFTPForm
addFTPForm.style.display = addFTPForm.style.display === "none" ? "block" : "none";
});
} else {
console.error("add new user button not found.");
}
// CHANGE PASSWORD
function changePassModal() {
// Handle the modal show event
$('#changePasswordModal').on('show.bs.modal', function (event) {
// Get the button that triggered the modal
var button = $(event.relatedTarget);
// Find the closest <tr> element to the button
var row = button.closest('tr');
var databaseUsername = row.find('#databaseUsername').text();
$('#changePasswordForm [name="username"]').val(databaseUsername);
});
};
// CHANGE PATH
function changePathModal() {
// Handle the modal show event
$('#changePathModal').on('show.bs.modal', function (event) {
// Get the button that triggered the modal
var button = $(event.relatedTarget);
// Find the closest <tr> element to the button
var row = button.closest('tr');
var databaseUsername = row.find('#databaseUsername').text();
var databasePath = row.find('#databasePath').text();
$('#changePathForm [name="username"]').val(databaseUsername);
$('#changePathForm [name="current_path"]').val(databasePath);
});
};
// DELETE USER
function confirmDelete(button) {
var countdown = 5;
var countdownActive = true; // Variable to track countdown status
// Change the button style and text
$(button).removeClass('btn-danger').addClass('btn-dark').html('<i class="bi bi-trash3-fill"></i> Confirm <span class="btn-indicator btn-indicator-mini bg-danger">' + countdown + '</span>');
// Interval to update countdown
var intervalId = setInterval(function () {
countdown--;
// Update the countdown value in the button text
$(button).find('.btn-indicator-mini').text(countdown);
// Remove the onclick event to prevent further changes on subsequent clicks
$(button).removeAttr('onclick');
// If countdown reaches 0, revert the button, clear the interval, and set countdownActive to false
if (countdown === 0) {
clearInterval(intervalId);
revertButton(button);
countdownActive = false;
}
}, 1000);
// Add a click event to the confirm button
$(button).on('click', function () {
// Check if countdown is active before allowing form submission
if (countdownActive) {
// Submit the parent form when the button is clicked during the countdown
$(button).closest('form').submit();
}
});
}
// Function to revert the button to its initial state
function revertButton(button) {
$(button).removeClass('btn-dark').addClass('btn-danger').html('<i class="bi bi-trash3"></i> Delete');
$(button).attr('onclick', 'confirmDelete(this);');
}
// SEARCH FTP USERS
function initializeFTPManagement() {
// Get reference to the search input
const searchFTPInput = document.getElementById("searchFTPInput");
// Get all domain rows (FTP account rows) to be used for filtering
const domainRows = document.querySelectorAll("#ftp-accounts-table tbody .domain_row");
// Handle search input changes
searchFTPInput.addEventListener("input", function() {
const searchTerm = searchFTPInput.value.trim().toLowerCase();
// Loop through domain rows and hide/show based on search term
domainRows.forEach(function(row) {
// Get the username span inside the td
const username = row.querySelector("td span").textContent.toLowerCase();
// Get the path element inside the second column
const pathElement = row.querySelector("td.desktop-only span");
const path = pathElement ? pathElement.textContent.toLowerCase() : '';
// Show row if search term matches username or path, otherwise hide it
if (username.includes(searchTerm) || path.includes(searchTerm)) {
row.style.display = "table-row";
} else {
row.style.display = "none";
}
});
});
}
// GENERATE PASS FOR NEW USER AND CHANGE PASS FORMS
function generateRandomStrongPassword(length) {
const lowercase = "abcdefghijklmnopqrstuvwxyz";
const uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const digits = "0123456789";
const special = "@.-_=+";
const allChars = lowercase + uppercase + digits + special;
let password = "";
// Ensure the password contains at least one of each required type
password += lowercase.charAt(Math.floor(Math.random() * lowercase.length));
password += uppercase.charAt(Math.floor(Math.random() * uppercase.length));
password += digits.charAt(Math.floor(Math.random() * digits.length));
password += special.charAt(Math.floor(Math.random() * special.length));
// Generate the rest of the password
for (let i = 4; i < length; i++) {
const randomIndex = Math.floor(Math.random() * allChars.length);
password += allChars.charAt(randomIndex);
}
// Shuffle the password to avoid predictable patterns
return password.split('').sort(() => 0.5 - Math.random()).join('');
}
// NEW USER FORM
function generateInitiallyFTPPassword() {
const generatedPassword = generateRandomStrongPassword(16);
document.getElementById("admin_password").value = generatedPassword;
document.getElementById("new_password").value = generatedPassword;
}
generateInitiallyFTPPassword();
document.getElementById("generatePassword").addEventListener("click", function() {
const generatedPassword = generateRandomStrongPassword(16);
document.getElementById("admin_password").value = generatedPassword;
const passwordField = document.getElementById("admin_password");
if (passwordField.type === "password") {
passwordField.type = "text";
}
});
document.getElementById("togglePassword").addEventListener("click", function() {
const passwordField = document.getElementById("admin_password");
if (passwordField.type === "password") {
passwordField.type = "text";
} else {
passwordField.type = "password";
}
});
// EDIT PASSWORD MODAL
document.getElementById("generatePasswordinModal").addEventListener("click", function() {
const generatedPassword = generateRandomStrongPassword(16);
document.getElementById("new_password").value = generatedPassword;
const passwordField = document.getElementById("new_password");
if (passwordField.type === "password") {
passwordField.type = "text";
}
});
document.getElementById("togglePasswordinModal").addEventListener("click", function() {
const passwordField = document.getElementById("new_password");
if (passwordField.type === "password") {
passwordField.type = "text";
} else {
passwordField.type = "password";
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,228 @@
{% extends 'base.html' %}
{% block content %}
<div class="row g-3">
<p>{{_('An inode is a data structure that keeps the information about a file on your hosting account. The number of inodes indicates the number of files and folders you have. This includes everything on your account, emails, files, folders, and anything you store on the server.')}}</p>
<div class="col-xl-7">
<div class="card card-one">
<div class="card-header">
<h6 class="card-title">{{_('Inodes usage per directory')}}</h6>
<nav class="nav nav-icon nav-icon-sm ms-auto">
<a href="#" onclick='location.reload(true); return false;' class="nav-link"><i class="bi bi-arrow-clockwise"></i></a>
</nav>
</div><!-- card-header -->
<div class="card-body">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<canvas id="folderChart" width="400" height="200"></canvas>
</div><!-- card-body -->
</div><!-- card -->
</div><!-- col -->
<div class="col-sm-12 col-xl-5">
<div class="card card-one">
<div class="card-header">
<h6 class="card-title">{{_('Browse Directories')}}</h6>
<nav class="nav nav-icon nav-icon-sm ms-auto">
<a href="/inodes-explorer" class="nav-link"><i class="bi bi-house-fill"></i></a>
</nav>
</div><!-- card-header -->
<div class="card-body">
<table id="folders_to_navigate" class="table table-hover table-striped">
<thead>
<tr>
<th>{{_('Directory')}}</th>
<th>{{_('Inodes')}}</th>
</tr>
</thead>
<tbody>
{% if request.path != '/inodes-explorer/' %}
<tr>
<td><a href="#" id="goUp"><i class="bi bi-arrow-90deg-up"></i> {{_('Up One Level')}}</a></td>
<td></td>
</tr>
{% endif %}
{% for line in total_inodes_output.split('\n') %}
{% if line.strip() %}
<tr>
{% set parts = line.split() %}
{% set count = parts[0] %}
{% set directory = parts[1:]|join(' ') %}
<td class="file-name"><a href="#" onclick="openDirectory('{{ directory }}'); return false;"><i style="color: orange;" class="ri-folder-2-fill"></i> {{ directory }}</a>
</td>
<td>{{ count }}</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div><!-- card-body -->
</div><!-- card -->
</div><!-- col -->
</div>
<script>
async function openDirectory(directory) {
// Display a toast message indicating that disk usage is being calculated for the specified directory
const toastMessage = `{{ _("Calculating inodes count for") }} ` + directory + `..`;
const toast = toaster({
body: toastMessage,
className: `border-0 text-white bg-primary`,
});
// Construct the new URL by appending the directory to the current URL
var currentUrl = window.location.href;
var separator = currentUrl.endsWith('/') ? '' : '/';
var newUrl = currentUrl + separator + directory;
try {
// Send a GET request to the server using the fetch API
const response = await fetch(newUrl);
// Get the response HTML content
const resultHtml = await response.text();
// Parse the HTML string to extract the content of the specific element
const parser = new DOMParser();
const doc = parser.parseFromString(resultHtml, 'text/html');
const mainScopeContent = doc.getElementById("main-scope")?.innerHTML;
// Replace the content of the element with the ID "main-scope"
const mainScopeElement = document.getElementById("main-scope");
if (mainScopeElement) {
mainScopeElement.innerHTML = mainScopeContent || '';
}
//add to url wothout reloading!
history.pushState({}, '', newUrl);
// Initialize the chart after updating the content and URL
initializeChart();
} catch (error) {
// Handle errors if the fetch request fails
console.error('Error fetching data:', error);
}
}
</script>
<script>
// Function to extract data from the table
function extractDataFromTable() {
const data = [];
const tableRows = document.querySelectorAll("#folders_to_navigate tbody tr");
tableRows.forEach((row) => {
const columns = row.querySelectorAll("td");
if (columns.length === 2) {
const directory = columns[0].textContent.trim();
const inodes = parseInt(columns[1].textContent);
// Exclude rows with "Up One Level"
if (directory !== "{{_('Up One Level')}}") {
data.push({ directory, inodes });
}
}
});
return data;
}
// Function to create a chart
function createChart(data) {
const labels = data.map((entry) => entry.directory);
const values = data.map((entry) => entry.inodes);
const ctx = document.getElementById("folderChart").getContext("2d");
const chart = new Chart(ctx, {
type: "bar",
data: {
labels: labels,
datasets: [
{
label: "{{_('Inodes Usage')}}",
data: values,
backgroundColor: "#506fd9",
},
],
},
});
// console.log(data);
}
// Function to initialize the chart on page load
function initializeChart() {
const tableData = extractDataFromTable();
createChart(tableData);
document.getElementById('goUp').addEventListener('click', async function() {
const toastMessage = `{{ _("Calculating inodes for parent directory..") }}`;
const toast = toaster({
body: toastMessage,
className: `border-0 text-white bg-primary`,
});
var currentUrl = window.location.href;
var parts = currentUrl.split('/');
parts.pop();
var newUrl = parts.join('/');
try {
const response = await fetch(newUrl);
if (!response.ok) {
throw new Error(`{{ _("Failed to fetch") }} ${newUrl}`);
}
const resultHtml = await response.text();
// Parse the HTML string to extract the content of the specific element
const parser = new DOMParser();
const doc = parser.parseFromString(resultHtml, 'text/html');
const mainScopeContent = doc.getElementById("main-scope")?.innerHTML;
// Replace the content of the element with the ID "main-scope"
const mainScopeElement = document.getElementById("main-scope");
if (mainScopeElement) {
mainScopeElement.innerHTML = mainScopeContent || '';
}
// Update the browser history
history.pushState({}, '', newUrl);
// Initialize the chart after updating the content and URL
initializeChart();
} catch (error) {
console.error('Error fetching data:', error);
}
});
}
</script>
<script type="module">
initializeChart();
</script>
{% endblock %}

View File

@@ -0,0 +1,708 @@
<!-- flarum.html -->
{% extends 'base.html' %}
{% block content %}
{% if domains %}
<style>
.toast-header {
background-color: #192030;
padding: 15px;
font-weight: 600;
font-size: 15px;
margin-bottom: 0;
line-height: 1.4;
color: rgb(255 255 255 / 84%)!important;
}
.no_link {
color: black;
text-decoration: none;
}
.nije-link {
text-decoration: none;
color: black;
border-bottom: 1px dashed black;
}
</style>
<!-- Flash messages -->
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
{% if "Error" in message %}
<script type="module">
const toastMessage = '{{ message }}';
const toast = toaster({
body: toastMessage,
header: `<div class="d-flex align-items-center" style="color: #495057;"><l-i class="bi bi-x-lg" class="me-2" style="color:red;"></l-i> {{ _("Flarum Installation failed.") }}</div>`,
});
</script>
{% else %}
<script type="module">
const toastMessage = '{{ message }}';
const toast = toaster({
body: toastMessage,
header: `<div class="d-flex align-items-center" style="color: #495057;"><l-i class="bi bi-check-lg" class="me-2" style="color:green;"></l-i> {{ _("Flarum successfully installed.") }}</div>`,
});
</script>
{% endif %}
{% endfor %}
{% endif %}
{% endwith %}
<!-- END Flash messages -->
<div class="row">
<div class="collapse mb-2" id="collapseExample">
<div class="card card-body">
<!-- Form for adding new containers -->
<div class="container">
<a href="#" class="nije-link" id="cancelLink" style="display: none;"><i class="bi bi-x-lg" style="right: 15px;top: 15px;position: absolute;color: black;padding: 6px 10px;border-radius: 50px;"></i></a>
<div class="row">
<div class="col-md-6 offset-md-3">
<h2 class="mb-3"><i class="bi bi-download"></i> {{ _("Install Flarum") }}</h2>
<p>{{ _("Install Flarum on an existing domain.") }}</p>
<form method="post" id="installForm" action="/flarum/install">
<div class="form-group row">
<div class="col-12">
<label for="website_name" class="form-label">{{ _("Website Name:") }}</label>
<input type="text" class="form-control" name="website_name" id="website_name" value="My Flarum" required>
</div>
</div>
<div class="form-group">
<label for="domain_id" class="form-label">{{ _("Domain:") }}</label>
<div class="input-group">
</select>
<select class="form-select" name="domain_id" id="domain_id">
{% for domain in domains %}
<option class="punycode" value="{{ domain.domain_id }}">{{ domain.domain_url }}</option>
{% endfor %}
</select>
<div class="input-group-append" style="width:30%;">
<input type="text" class="form-control" name="subdirectory" id="subdirectory" placeholder="subfolder">
</div>
</div>
</div>
<div class="form-group">
<label for="admin_email">{{ _("Admin Email:") }}</label>
<input type="email" class="form-control" name="admin_email" id="admin_email" required>
</div>
<script>
$(document).ready(function() {
// Check if the URL contains the parameter "install"
const urlParams = new URLSearchParams(window.location.search);
const installParam = urlParams.get('install');
if (installParam || window.location.hash === '#install') {
// Show the Bootstrap collapsible element
$("#collapseExample").collapse('show');
}
// Add event listener to the dropdown
$("#domain_id").change(function() {
// Get the selected domain URL
var selectedDomain = $("#domain_id option:selected").text();
// Update the admin email input value
var adminEmailInput = $("#admin_email");
var currentAdminEmail = adminEmailInput.val();
var atIndex = currentAdminEmail.indexOf('@');
if (atIndex !== -1) {
// If '@' exists in the email, replace the part after it with the selected domain
adminEmailInput.val(currentAdminEmail.substring(0, atIndex + 1) + selectedDomain);
} else {
// If '@' doesn't exist in the email, add the selected domain after 'admin@'
adminEmailInput.val('admin@' + selectedDomain);
}
});
// Add event listener to the "Install Flarum" button to toggle form and jumbotron
$("#collapseExample").on('shown.bs.collapse', function () {
$("#jumbotronSection").hide();
$("#cancelLink").show();
});
// Add event listener to the "Cancel" link to toggle form and jumbotron
$("#cancelLink").click(function() {
$("#collapseExample").collapse('hide');
$("#jumbotronSection").show();
$("#cancelLink").hide();
});
});
var selectedDomain = $("#domain_id option:selected").text();
// Update the admin email input value
var adminEmailInput = $("#admin_email");
var currentAdminEmail = adminEmailInput.val();
var atIndex = currentAdminEmail.indexOf('@');
if (atIndex !== -1) {
// If '@' exists in the email, replace the part after it with the selected domain
adminEmailInput.val(currentAdminEmail.substring(0, atIndex + 1) + selectedDomain);
} else {
// If '@' doesn't exist in the email, add the selected domain after 'admin@'
adminEmailInput.val('admin@' + selectedDomain);
}
</script>
<div class="form-group row">
<div class="col-md-6">
<label for="admin_username" class="form-label">{{ _("Admin Username:") }}</label>
<input type="text" class="form-control" name="admin_username" id="admin_username" required>
</div>
<div class="col-md-6">
<label for="admin_password" class="form-label">{{ _("Admin Password:") }}</label>
<div class="input-group">
<input type="password" class="form-control" name="admin_password" id="admin_password" required>
<div class="input-group-append">
<button class="btn btn-outline-success" type="button" id="generatePassword">
{{ _("Generate") }}
</button>
<button class="btn btn-outline-secondary" type="button" id="togglePassword">
<i class="bi bi-eye"></i>
</button>
</div>
</div>
</div>
</div>
<script>
function generateRandomUsername(length) {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let result = "";
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * charset.length);
result += charset.charAt(randomIndex);
}
return result;
}
function generateRandomStrongPassword(length) {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+";
let result = "";
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * charset.length);
result += charset.charAt(randomIndex);
}
return result;
}
function generateInitiallyUsernameAndPassword() {
const generatedUsername = generateRandomUsername(8);
const generatedPassword = generateRandomStrongPassword(16);
document.getElementById("admin_username").value = generatedUsername;
document.getElementById("admin_password").value = generatedPassword;
};
generateInitiallyUsernameAndPassword();
document.getElementById("generatePassword").addEventListener("click", function() {
const generatedPassword = generateRandomStrongPassword(16);
document.getElementById("admin_password").value = generatedPassword;
const passwordField = document.getElementById("admin_password");
if (passwordField.type === "password") {
passwordField.type = "text";
}
});
document.getElementById("togglePassword").addEventListener("click", function() {
const passwordField = document.getElementById("admin_password");
if (passwordField.type === "password") {
passwordField.type = "text";
} else {
passwordField.type = "password";
}
});
</script>
<div class="form-group">
<label for="flarum_version" class="form-label">Flarum Version:</label>
<div class="form-field">
<select class="form-select" name="flarum_version" id="flarum_version">
</select>
</div>
</div>
<!-- https://github.com/flarum/flarum/releases -->
<br>
<button type="submit" class="btn btn-primary" id="installButton">{{ _("Start Installation") }}</button>
</form>
<script>
// Fetch the Flarum versions from the GitHub API
fetch('https://api.github.com/repos/flarum/flarum/tags')
.then(response => response.json())
.then(data => {
// Extract tag names from the response
const versions = data.map(tag => tag.name);
// Filter out versions if needed (for example, excluding beta or RC versions)
const filteredVersions = versions.filter(version => !version.includes('beta') && !version.includes('RC') && !version.includes('-'));
// Take the latest 10 versions
const latestVersions = filteredVersions.slice(0, 1);
// Populate the select dropdown with options
const selectElement = document.getElementById('flarum_version');
latestVersions.forEach(version => {
const option = document.createElement('option');
option.value = version;
option.textContent = `${version}`;
selectElement.appendChild(option);
});
})
.catch(error => {
console.error('Error fetching Flarum versions:', error);
// In case of an error, you might want to display a default option
const selectElement = document.getElementById('flarum_version');
const option = document.createElement('option');
option.value = ''; // Set a value for the default option if needed
option.textContent = 'Error fetching versions';
selectElement.appendChild(option);
});
// Function to compare two version strings
function compareVersions(a, b) {
const versionA = a.split('.').map(Number);
const versionB = b.split('.').map(Number);
for (let i = 0; i < Math.max(versionA.length, versionB.length); i++) {
const partA = versionA[i] || 0;
const partB = versionB[i] || 0;
if (partA < partB) return -1;
if (partA > partB) return 1;
}
return 0;
}
</script>
</div>
</div>
</div>
</div>
</div>
{% if data %}
{% if view_mode == 'table' %}
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>{{ _("Domain") }}</th>
<th>{{ _("Flarum Version") }}</th>
<th>{{ _("Admin Email") }}</th>
<th>{{ _("Created on") }}</th>
<th>{{ _("Actions") }}</th>
</tr>
</thead>
<tbody>
{% for row in data %}
<div class="modal fade" id="removeModal{{ row[6] }}" tabindex="-1" role="dialog" aria-labelledby="removeModalLabel{{ row[6] }}" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="removeModalLabel{{ row[6] }}">{{ row[0] }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body row">
<div class="col-md-6">
<h4>{{ _("Delete Flarum website") }}</h4>
<p>{{ _("This will irreversibly delete the website, permanently deleting all files and database.") }}</p>
<button type="button" class="btn btn-danger" onclick="confirmRemove('{{ row[6] }}')">{{ _("Uninstall") }}</button>
</div>
<div class="col-md-6">
<h4>{{ _("Remove from the Manager") }}</h4>
<p>{{ _("This will just remove the installation from the Manager but keep the files and database.") }}</p>
<button type="button" class="btn btn-warning" onclick="confirmDetach('{{ row[6] }}')">{{ _("Detach") }}</button>
</div>
</div>
<div class="modal-footer">
</div>
</div>
</div>
</div>
{% set domain_url = row[0] %}
<tr>
<td><a class="punycode no_link domain_link" href="http://{{ row[0] }}" target="_blank"><img src="https://www.google.com/s2/favicons?domain={{ row[0] }}" alt="{{ row[0] }} Favicon" style="width:16px; height:16px; margin-right:5px;">
{{ row[0] }} <i class="bi bi-box-arrow-up-right"></i></a></td>
<td>{{ row[3] }}</td>
<td>{{ row[2] }}</td>
<td>{{ row[4] }}</td>
<td>
<a class="btn btn-secondary mx-2" href="/website?domain={{ row[0] }}">{{ _("Manage") }}</a>
<button type="button" class="btn btn-danger" style="border: 0px;" data-bs-toggle="modal" data-bs-target="#removeModal{{ row[1] }}"><i class="bi bi-trash3"></i> {{ _("Remove") }}</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% if view_mode == 'cards' %}
<style>
/* Style for the container */
.image-container {
position: relative;
display: inline-block;
}
/* Style for the button */
.center-button {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: none; /* Hide the button by default */
z-index: 1; /* Ensure the button appears above the image */
}
/* Show the button when hovering over the image container */
.image-container:hover .center-button {
display: block;
}
.card {
border: none;
border-radius: 10px
}
.c-details span {
font-weight: 300;
font-size: 13px
}
.icon {
width: 50px;
height: 50px;
background-color: #eee;
border-radius: 15px;
display: flex;
align-items: center;
justify-content: center;
font-size: 39px
}
.badge span {
background-color: black;
width: 80px;
height: 20px;
padding-bottom: 3px;
border-radius: 5px;
display: flex;
color: white;
justify-content: center;
align-items: center
}
.text1 {
font-size: 14px;
font-weight: 600
}
.text2 {
color: #a5aec0
}
a.close_button {
right: 5px;top: 10px;position: absolute;color: white;padding: 0px 4px;background: indianred;border-radius: 50px; z-index:1;
}
a.close_button:hover {
background: red;
}
</style>
<div class="container mb-3">
<div class="row">
{% for row in data %}
<div class="modal fade" id="removeModal{{ row[6] }}" tabindex="-1" role="dialog" aria-labelledby="removeModalLabel{{ row[6] }}" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="removeModalLabel{{ row[6] }}">{{ row[0] }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body row">
<div class="col-md-6">
<h4>{{ _("Delete Flarum website") }}</h4>
<p>{{ _("This will irreversibly delete the website, permanently deleting all files and database.") }}</p>
<button type="button" class="btn btn-danger" onclick="confirmRemove('{{ row[6] }}')">{{ _("Uninstall") }}</button>
</div>
<div class="col-md-6">
<h4>{{ _("Remove from the Manager") }}</h4>
<p>{{ _("This will just remove the installation from the Manager but keep the files and database.") }}</p>
<button type="button" class="btn btn-warning" onclick="confirmDetach('{{ row[6] }}')">{{ _("Detach") }}</button>
</div>
</div>
<div class="modal-footer">
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card bg-light mb-2">
<a href="#" class="close_button" data-bs-toggle="modal" data-bs-target="#removeModal{{ row[6] }}"><i class="bi bi-x-lg" style=""></i></a>
<div class="mt-0">
<div class="image-container">
<a href="/website?domain={{ row[0] }}">
<img
id="screenshot-image-{{ row[0] }}"
src="/static/images/placeholder.svg"
alt="Screenshot of {{ row[0] }}"
class="img-fluid"
/>
</a>
<a href="/website?domain={{ row[0] }}" class="center-button btn btn-dark">
<i class="bi bi-sliders2-vertical"></i> {{ _("Manage") }}
</a>
</div>
</div>
<div class="d-flex p-2 justify-content-between">
<div class="d-flex flex-row align-items-center">
<div class="icon"> <i class=""><svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="42px" height="42px" viewBox="0 0 64 64" version="1.1" preserveAspectRatio="xMidYMid" id="svg22" sodipodi:docname="flarum-icon.svg" inkscape:version="0.92.4 (5da689c313, 2019-01-14)"> <metadata id="metadata26"> <rdf:rdf> <cc:work rdf:about=""> <dc:format>image/svg+xml</dc:format> <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"></dc:type> <dc:title></dc:title> </cc:work> </rdf:rdf> </metadata> <sodipodi:namedview pagecolor="#ffffff" bordercolor="#666666" borderopacity="1" objecttolerance="10" gridtolerance="10" guidetolerance="10" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-width="1920" inkscape:window-height="1001" id="namedview24" showgrid="false" inkscape:zoom="2.6484375" inkscape:cx="104.78113" inkscape:cy="7.0183228" inkscape:window-x="-9" inkscape:window-y="-9" inkscape:window-maximized="1" inkscape:current-layer="svg22"></sodipodi:namedview> <defs id="defs12"> <linearGradient x1="39.25486" y1="79.948608" x2="39.25486" y2="1.4437387" id="linearGradient-1" gradientTransform="matrix(0.13724088,0,0,0.27795633,15.103245,19.574026)" gradientUnits="userSpaceOnUse"> <stop stop-color="#D22929" offset="0%" id="stop2"></stop> <stop stop-color="#B71717" offset="100%" id="stop4"></stop> </linearGradient> <linearGradient x1="0.5" y1="0" x2="0.5" y2="1" id="linearGradient-2"> <stop stop-color="#E7762E" offset="0%" id="stop7"></stop> <stop stop-color="#E7562E" offset="100%" id="stop9"></stop> </linearGradient> <linearGradient inkscape:collect="always" xlink:href="#linearGradient-2" id="linearGradient833" x1="45.281994" y1="0" x2="45.281994" y2="90.563988" gradientTransform="matrix(0.20819775,0,0,0.18322471,15.103245,19.574026)" gradientUnits="userSpaceOnUse"></linearGradient> </defs> <g id="g842" transform="matrix(2.8799999,0,0,2.8799999,-38.648859,-56.373193)"> <path d="M 15.10814,34.404271 15.10354,20.58171 c -1.84e-4,-0.556529 0.383134,-0.768017 0.854123,-0.473649 l 9.919955,6.199972 v 15.488216 l -9.293169,-5.640035 c -1.287893,-0.701196 -1.47454,-1.1364 -1.476347,-1.751943 z" id="path14" inkscape:connector-curvature="0" style="fill:url(#linearGradient-1);stroke-width:0.1953125"></path> <path d="m 16.114255,19.574026 c -0.558366,0 -1.01101,0.451467 -1.01101,1.012451 v 13.802364 c 0.02805,0.474415 0.0039,0.969043 1.510295,1.778745 0,0 -1.476019,-1.434143 0.846607,-1.442045 H 33.958464 V 19.574026 Z" id="path16" inkscape:connector-curvature="0" style="fill:url(#linearGradient833);stroke-width:0.1953125"></path> </g> </svg></i> </div>
<div class="ms-2 c-details">
<h6 class="punycode mb-0"><a href="http://{{ row[0] }}" target="_blank" class="nije-link">{{ row[0] }}</a></h6> <span>{{ _("Flarum") }} {{ row[3] }}</span>
</div>
</div>
<div class="badge" style="display: -webkit-box;">
<!--div class="dropdown" style="vertical-align: middle; display: inline;">
<a href="" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-three-dots-vertical" style="font-size: 1.3125rem;"></i>
</a>
<ul class="dropdown-menu" aria-labelledby="more-actions">
<li><a class="dropdown-item" href="#">PHP Version</a></li>
<li><a class="dropdown-item" href="#">SSL Security</a></li>
<li><a class="dropdown-item" href="#">SSL Security</a></li>
<hr>
<li><a class="dropdown-item" href="#">Refresh Preview</a></li>
</ul>
</div--></div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<script>
// Function to update a screenshot image
function updateScreenshot(screenshotImage, screenshotURL) {
var imageLoader = new Image();
imageLoader.src = screenshotURL;
imageLoader.onload = function() {
screenshotImage.src = screenshotURL;
};
}
// Function to update all screenshot images
function updateAllScreenshots() {
{% for row in data %}
var screenshotImage = document.getElementById("screenshot-image-{{ row[0] }}");
var screenshotURL = "/screenshot/{{ row[0] }}";
updateScreenshot(screenshotImage, screenshotURL);
{% endfor %}
}
updateAllScreenshots();
</script>
{% endif %}
<script>
// Function to confirm removal
function confirmRemove(id) {
// Send an AJAX request to the server to remove Flarum
var xhr = new XMLHttpRequest();
xhr.open('POST', '/flarum/remove', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
// Reload the page to update the table
location.reload();
} else {
alert('{{ _("An error occurred while removing Flarum.") }}');
}
}
};
xhr.send('id=' + id);
}
// Function to confirm detachment
function confirmDetach(id) {
// Send an AJAX request to the server to remove Flarum
var xhr = new XMLHttpRequest();
xhr.open('POST', '/flarum/detach', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
// Redirect to wp manager page
window.location.href = '/flarum';
} else {
alert('{{ _("An error occurred while detaching Flarum.") }}');
}
}
};
xhr.send('id=' + id);
}
</script>
</div>
<style>
.nije-link {
text-decoration: none;
color: black;
border-bottom: 1px dashed black;
}
</style>
{% else %}
<!-- Display jumbotron for no wp installations -->
<div class="jumbotron text-center mt-5" id="jumbotronSection" style="min-height: 70vh;display:block;">
<h1>{{ _("No Flarum Installations") }}</h1>
<p>{{ _("There are no existing Flarum installations. You can install a new instance below.") }}</p>
<button class="btn btn-lg btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#collapseExample" aria-expanded="false" aria-controls="collapseExample">
<i class="bi bi-download"></i> {{ _("Install Flarum") }}
</button>
</div>
{% endif %}
{% else %}
<!-- Display jumbotron for no domains -->
<div class="jumbotron text-center mt-5" id="jumbotronSection">
<h1>{{ _("No Domains") }}</h1>
<p>{{ _("Add a domain name first in order to install Flarum.") }}</p>
<a href="/domains#add-new" class="btn btn-lg btn-primary">
<i class="bi bi-plus-lg"></i> {{ _("Add a Domain Name") }}
</a>
</div>
{% endif %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/punycode/2.1.1/punycode.min.js"></script>
<script type="module">
punnyCodeShouldBeGlobalFunction function() {
var punycodeElements = document.querySelectorAll(".punycode");
punycodeElements.forEach(function(element) {
element.textContent = punycode.toUnicode(element.textContent);
});
};
punnyCodeShouldBeGlobalFunction();
</script>
</section>
<footer class="main-footer btn-toolbar" role="toolbar">
<div class="btn-group" role="group" aria-label="Status">
</div>
<div class="ms-auto" role="group" aria-label="Actions">
<button class="btn btn-transparent" type="button" aria-expanded="false" id="refreshData">
<i class="bi bi-arrow-counterclockwise"></i> {{ _('Refresh') }}<span class="desktop-only"> {{ _('Data') }}</span>
</button>
<button class="btn btn-primary mx-2" type="button" data-bs-toggle="collapse" data-bs-target="#collapseExample" aria-expanded="false" aria-controls="collapseExample">
<i class="bi bi-download"></i> {{ _('Install') }}<span class="desktop-only"> {{ _('Flarum') }}</span>
</button>
<span class="desktop-only">{% if view_mode == 'cards' %}
<a href="{{ url_for('list_flarum', view='table') }}" data-bs-toggle="tooltip" data-bs-placement="top" title="{{ _('Switch to List View - perfect for those with many Websites.') }}" class="btn btn-outline" style="padding-left: 5px; padding-right: 5px;"><i class="bi bi-table"></i></a>
{% endif %}
{% if view_mode == 'table' %}
<a href="{{ url_for('list_flarum', view='cards') }}" data-bs-toggle="tooltip" data-bs-placement="top" title="{{ _('Switch to Grid View - a visual representation of your Websites and their content.') }}" class="btn btn-outline" style="padding-left: 5px; padding-right: 5px;"><i class="bi bi-view-list"></i></a>
{% endif %}</span>
<script>
// Add an event listener to the links to hide tooltip as its buggy!
document.addEventListener('DOMContentLoaded', function () {
var links = document.querySelectorAll('.desktop-only a');
links.forEach(function (link) {
link.addEventListener('click', function () {
// Hide the tooltip when the link is clicked
var tooltip = new bootstrap.Tooltip(link);
tooltip.hide();
});
});
});
</script>
</div>
</footer>
{% endblock %}

View File

@@ -0,0 +1,153 @@
<!-- ip_blocker.html -->
{% extends 'base.html' %}
{% block content %}
<!-- content for the ssl page -->
{% if domains %}
<style>
@media (min-width: 768px) {
table.table {
table-layout: fixed;
}
table.table td {
word-wrap: break-word;
}
}
@media (max-width: 767px) {
span.advanced-text {display:none;}
}
.hidden {
display: none;
}
.advanced-settings {
align-items: center;
text-align: right;
color: black;
}
.advanced-settings i {
margin right: 5px;
transform: rotate(-90deg);
transition: transform 0.3s ease-in-out;
}
.domain_link {
border-bottom: 1px dashed #999;
text-decoration: none;
color: black;
}
.advanced-settings.active i {
transform: rotate(0);
}
thead {
border: 1px solid rgb(90 86 86 / 11%);
}
th {
color: rgb(0 0 0 / 65%)!important;
background: #fafafa!important;
text-transform: uppercase;
font-weight: 400;
}
</style>
<div class="row">
<!-- Search bar -->
<div class="input-group mb-3" style="padding-left:0px;padding-right:0px;">
<input type="text" class="form-control" placeholder="{{ _('Search domains') }}" id="searchDomainInput">
</div>
<script>
document.addEventListener("DOMContentLoaded", function() {
// Get references to search input and display settings div
const searchDomainInput = document.getElementById("searchDomainInput");
const displaySettingsDiv = document.querySelector(".display_settings");
// Get all domain rows to be used for filtering
const domainRows = document.querySelectorAll(".domain_row");
// Handle search input changes
searchDomainInput.addEventListener("input", function () {
const searchTerm = searchDomainInput.value.trim().toLowerCase();
// Loop through domain rows and hide/show based on search term
domainRows.forEach(function (row) {
// Move the definition of domainName inside the loop
const domainName = row.querySelector("td:nth-child(1)").textContent.toLowerCase();
// Show row if search term matches domain name or domain URL, otherwise hide it
if (domainName.includes(searchTerm)) {
row.style.display = "table-row";
} else {
row.style.display = "none";
}
});
});
});
</script>
<table class="table" id="ssl_table">
<thead>
<tr>
<th>{{ _('Domain') }}</th>
<th>{{ _('Blocked IPs') }}</th>
</tr>
</thead>
<tbody>
{% for domain in domains %}
<tr class="domain_row">
<td>{{ domain.domain_url }}</td>
<td>
<div class="d-flex gap-2 mt-3 mt-md-0">
<span id="previous_value_for_domain" style="display:none;">{{ domain_data[domain.domain_url] }}</span>
<form action="/ip_blocker" method="post">
<div class="form-check form-switch">
<textarea id="ip_blocker_{{ domain.domain_url }}" name="ip_blocker">
{{ domain_data[domain.domain_url] }}
</textarea>
</div>
<input type="hidden" name="domain_name" value="{{ domain.domain_url }}">
<button type="submit" class="btn btn-primary" id="ip_blocker">{{ _('Save') }}</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<!-- Display jumbotron for no domains -->
<div class="jumbotron text-center mt-5" id="jumbotronSection">
<h1>{{ _('No Domains') }}</h1>
<a href="/domains#add-new" class="btn btn-lg btn-primary">
<i class="bi bi-plus-lg"></i> {{ _('Add a Domain Name') }}
</a>
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,163 @@
{% extends 'base.html' %}
{% block content %}
<p>{{ _('The Clam AntiVirus Scanner (ClamAV) antivirus software searches your files for malicious programs. If the scanner identifies a potential security threat, it flags the file to allow you to take the appropriate action.') }}</p>
<div class="container">
<div class="col-auto">
<label class="directory-select-label" for="directory-select">{{ _('Choose a directory') }}</label><br>
<div class="input-group">
<input type="text" id="directory-select" class="form-control" list="directory-options">
<datalist id="directory-options">
{% for directory in directories %}
<option value="{{ directory }}">
{% endfor %}
</datalist>
<span class="input-group-btn">
<button id="start-scan-btn" class="btn btn-primary" tabindex="-1">{{ _('Start Scan') }}</button>
<!-- Scanning Spinner Button (Initially hidden) -->
<button id="scanning-btn" class="btn btn-primary" tabindex="-1" type="button" style="display: none;" disabled>
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
{{ _('Scanning...') }}
</button>
</span>
</div>
</div>
<!-- Scan Complete Message (Initially hidden) -->
<div id="scan-complete-message" class="alert alert-success mt-3 mb-3" style="display: none;">
{{ _('Scan is complete!') }}
</div>
<!-- Scan Results Card (Initially hidden) -->
<div id="scan-results-card" class="card card-one mt-3" style="display: none;">
<div class="card-header" style="display: block;">
<h6 class="card-title">{{ _('Scan Results') }}</h6>
<nav class="nav nav-icon nav-icon-sm ms-auto">
<a href="" class="nav-link"><i class="ri-refresh-line"></i></a>
<a href="" class="nav-link"><i class="ri-more-2-fill"></i></a>
</nav>
</div>
<div class="card-body">
<div class="row row-cols-auto gy-3 gx-5">
<div class="col-auto">
<!-- Scan Results -->
<label class="fs-xs mb-1"><div id="scan-results"></div></label>
</div>
</div>
</div>
</div>
</div>
<script>
// Function to handle displaying scan results in real-time
// Function to handle displaying scan results in real-time
function displayScanResults(result) {
const scanResultsDiv = document.getElementById('scan-results');
// Split the current content into an array of lines
const lines = scanResultsDiv.innerHTML.split('<br>');
// Check if the result starts with "Scanning"
if (result.startsWith('Scanning')) {
// Append the result to the array of lines
lines.push(result);
} else {
// If it does not start with "Scanning," replace the last line
lines[lines.length - 1] = result;
}
// Update the inner HTML with the modified lines
scanResultsDiv.innerHTML = lines.join('<br>');
}
// Function to show the scan complete message
function showScanCompleteMessage() {
document.getElementById('scan-complete-message').style.display = 'block';
}
// Function to initiate the scan when the "Start Scan" button is clicked
document.getElementById('start-scan-btn').addEventListener('click', function() {
// Hide the "Start Scan" button and show the scanning spinner button
document.getElementById('start-scan-btn').style.display = 'none';
document.getElementById('scanning-btn').style.display = 'inline-block';
// Get the selected directory from the dropdown
const selectedDirectory = document.getElementById('directory-select').value;
// Send the selected directory to the server for scanning
fetch(`/start-scan?directory=${encodeURIComponent(selectedDirectory)}`)
.then(response => {
if (!response.ok) {
throw new Error('{{ _("Network response was not ok") }}');
}
return response.body.getReader();
})
.then(reader => {
document.getElementById('scan-results-card').style.display = 'block';
// Read and display scan results in real-time
const decoder = new TextDecoder('utf-8');
return reader.read().then(function processText({ done, value }) {
if (done) {
showScanCompleteMessage();
return;
}
const result = decoder.decode(value);
displayScanResults(result);
return reader.read().then(processText);
});
})
.catch(error => {
console.error('Scan failed:', error);
displayScanResults('Scan failed: ' + error.message);
document.getElementById('start-scan-btn').style.display = 'block';
})
.finally(() => {
// Show the scan results card after the scan is complete
document.getElementById('scanning-btn').style.display = 'none';
document.getElementById('start-scan-btn').style.display = 'block';
});
});
function inputScanDomain() {
const urlParams = new URLSearchParams(window.location.search);
const pathValue = urlParams.get('path');
const directoryInput = document.getElementById("directory-select");
if (pathValue) {
directoryInput.value = pathValue;
document.getElementById('start-scan-btn').click();
}
}
// Listen for changes in the URL's query parameters
window.addEventListener("hashchange", inputScanDomain);
window.addEventListener("popstate", inputScanDomain);
window.addEventListener("load", inputScanDomain);
</script>
{% endblock %}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,865 @@
<!-- manager/nodejs.html -->
{% extends 'base.html' %}
{% block content %}
<div class="modal fade" id="installPIPModal" tabindex="-1" role="dialog" aria-labelledby="installPIPModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="installPIPModalLabel">{{ _('Application Logs') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ _('Close') }}"></button>
</div>
<div class="modal-body bg-dark">
<div id="pipinstalllogprogress" class="mb-0"></div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="viewModal" tabindex="-1" role="dialog" aria-labelledby="viewModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="viewModalLabel">{{ _('Application Logs') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ _('Close') }}"></button>
</div>
<div class="modal-body bg-dark">
<div id="fileContent" class="mb-0"></div>
</div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/punycode/2.1.1/punycode.min.js"></script>
<style>
.st1 {
fill: #192030;
}
/* Style for dark mode svg icons */
[data-skin="dark"] .st1 {
fill: #506fd9;
}
</style>
<style>
.nije-link {
text-decoration: none;
color: black;
border-bottom: 1px dashed black;
}
.ikona:hover {
background: aliceblue;
}
#action_description p {
display: none;
margin-bottom: 0px;
margin-top: 1rem;
}
</style>
{% if current_domain %}
<div class="card">
<div class="row">
<div class="col-md-auto">
{% include 'partials/screenshot.html' %}
</div>
<div class="col">
<a href="#" onclick="updateScreenshot()" id="refresh_site_screenshot">
<i class="bi bi-arrow-clockwise" style="left: 15px;top: 15px;position: absolute; padding: 6px 10px;" data-bs-toggle="tooltip" data-bs-placement="right" data-bs-title="Refresh website screenshot"></i>
</a>
<div class="row">
<div class="col">
<div class="card-body">
<small class="card-text">Status</small>
<p class="card-text {% if pm2_data[8] == 'stopped' %}text-danger{% elif pm2_data[8] == 'online' %}text-success{% else %}text-warning{% endif %}">● {{ pm2_data[8] }}</p>
</div>
</div>
<div class="col">
<div class="card-body">
<small class="card-text">{{ _('Type') }}</small>
<p class="card-text"><i class=""><img src="/static/images/icons/nodejs.png" style="height: 1em;"></i> {{ container.type }}</p>
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="card-body">
<small class="card-text">{{ _('Files') }} <span id="filesSize">({{ _('Calculating size...') }})</span></small>
<p class="card-text"><a class="nije-link" href="/files/{{ current_domain }}">{{ domain_directory }}</a></p>
</div>
</div>
<div class="col">
<div class="card-body">
<small class="card-text">{{ _('Domain:') }}</small>
<p class="card-text">
<span id="favicon"></span> <a class="punycode nije-link" href="/domains">
{{ current_domain }}
</a>
</p>
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="card-body">
<small class="card-text">
<a class="nije-link" data-bs-toggle="collapse" href="#collapseAppInfo" aria-expanded="false" aria-controls="collapseAppInfo">{{ _('Status Information') }}</a>
</small>
<p class="card-text">
<div class="collapse" id="collapseAppInfo">
<ul class="list-group">
<li class="list-group-item"><small>{{ _('Uptime:') }}</small> <span><b>{{ pm2_data[6] }}</b></span></li>
<li class="list-group-item"><small>{{ _('Restarts:') }}</small> <span><b>{{ pm2_data[7] }}</b></span></li>
<li class="list-group-item"><small>{{ _('Watching:') }}</small> <span><b>{{ pm2_data[12] }}</b></span></li>
</ul>
</div>
</div>
</div>
<div class="col">
<div class="card-body">
<small class="card-text">{{ _('Created') }} <a data-bs-toggle="tooltip" data-bs-placement="right" data-bs-title="{{ container.created_date }}"><i class="bi bi-info-circle"></i></a></small>
</button>
<p class="card-text">
<span id="created_date">{{ container.created_date }}</span>
</p>
<script>
function GetFIlesSize() {
const filesSizeSpan = document.getElementById("filesSize");
const currentDomain = "{{current_domain}}";
// Fetch files size
fetch(`/get-files-size?selected_domain=${encodeURIComponent(currentDomain)}`)
.then(response => response.json())
.then(data => {
filesSizeSpan.textContent = "(" + data.size + ")";
})
.catch(error => {
console.error("An error occurred:", error);
});
}
GetFIlesSize()
</script>
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="card-body">
<small class="card-text">{{ _('NodeJS version:') }}</small>
<h4 class="card-text"><span id="nodejs-version"></span></h4>
</div>
</div><div class="col">
<div class="card-body">
<small class="card-text">{{ _('CPU usage:') }}</small>
<h4 class="card-text"><i class="dashboard_icon bi bi-cpu"></i> {{ pm2_data[9] }}</h4>
</div>
</div>
<div class="col">
<div class="card-body">
<small class="card-text">{{ _('Memory usage:') }}</small>
<h4 class="card-text"><i class="dashboard_icon bi bi-memory"></i> {{ pm2_data[10] }}</span></h4>
</div>
</div>
</div>
</div>
</div>
</div>
<!--p>domain id: {{ container.domain_id }}</p-->
<div class="row my-4">
<div role="group" aria-label="WP Actions" style="width: 100%; margin-top: 0px;" class="btn-group">
{% if 'temporary_links' in enabled_modules %}
<button type="button" class="btn btn-outline-dark btn-lg" data-bs-description="preview" data-selected-domain="{{ current_domain }}" data-bs-toggle="modal" data-bs-target="#previewModal" onclick="sendDataToPreview(event)"><span class="desktop-only">{{ _('Preview') }}</span><span class="mobile-only"><i class="bi bi-box-arrow-up-right"></i></span></button>
<script>
function sendDataToPreview(event) {
const button = event.currentTarget;
const domain = button.getAttribute('data-selected-domain');
fetch(`/domains/temporary-link?domain=${encodeURIComponent(domain)}`)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
if (data.link) {
window.open(data.link, '_blank');
} else {
console.error('No link found in the response.');
}
})
.catch(error => {
console.error('Error:', error);
});
}
</script>
{% endif %}
<button type="button" class="btn btn-outline-primary view-button btn-lg" data-file="{{ pm2_data[1] }}" data-type="logs" data-bs-description="logs">
<span class="desktop-only">{{ _('View Error Log') }}</span><span class="mobile-only"><i class="bi bi-bug"></i></span>
</button>
{% if pm2_data[8] == 'stopped' %}
<button type="button" class="btn btn-outline-success btn-lg" data-bs-description="start" onclick="startApp(this);">
<i class="bi bi-play-fill"></i><span class="desktop-only"> {{ _('Start') }}</span>
</button>
<script>
function startApp(button) {
// Get the PM2 process ID
const pm2Id = "{{ pm2_data[1] }}";
// Create a new XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.open('POST', `/pm2/start/${pm2Id}`, true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
// Optionally handle the response
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
// Handle success (e.g., show a success message or refresh the page)
console.log('Start successful:', xhr.responseText);
// Optionally refresh the application list or update the UI
} else {
// Handle error (e.g., show an error message)
console.error('Start failed:', xhr.statusText);
}
};
// Send the request
xhr.send();
}
</script>
{% else %}
<button type="button" class="btn btn-outline-danger btn-lg" data-bs-description="stop" onclick="stopApp(this);">
<i class="bi bi-stop-fill"></i><span class="desktop-only"> {{ _('Stop') }}</span>
</button>
<script>
function stopApp(button) {
// Get the PM2 process ID
const pm2Id = "{{ pm2_data[1] }}";
// Create a new XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.open('POST', `/pm2/stop/${pm2Id}`, true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
// Optionally handle the response
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
// Handle success (e.g., show a success message or refresh the page)
console.log('Stop successful:', xhr.responseText);
// Optionally refresh the application list or update the UI
} else {
// Handle error (e.g., show an error message)
console.error('Stop failed:', xhr.statusText);
}
};
// Send the request
xhr.send();
}
</script>
{% endif %}
<button type="button" data-bs-description="restart" id="restartButton" class="btn btn-primary btn-lg"></i><i class="bi bi-arrow-clockwise"></i><span class="desktop-only"> {{ _('Restart') }}</span></button>
<script>
document.getElementById('restartButton').addEventListener('click', function(event) {
event.preventDefault(); // Prevent the default button action
// Get the PM2 process ID
const pm2Id = "{{ pm2_data[1] }}";
// Create a new XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.open('POST', `/pm2/restart/${pm2Id}`, true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
// Optionally handle the response
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
// Handle success (e.g., show a success message)
console.log('Restart successful:', xhr.responseText);
} else {
// Handle error (e.g., show an error message)
console.error('Restart failed:', xhr.statusText);
}
};
// Send the request
xhr.send();
});
</script>
<button type="button" class="btn btn-danger btn-lg" data-bs-description="uninstall" onclick="confirmAppDelete(this);"><i class="bi bi-trash3"></i><span class="desktop-only"> {{ _('Uninstall') }}</span></button>
</div>
<div id="action_description" class="">
<p id="blank-id">&nbsp;</p>
<p id="preview-description">{{ _("Preview website with a temporary domain,") }} <b>{{ _("valid for 15 minutes only!") }}</b> {{ _("Helpful if your domain hasn't been pointed to the server's IP address yet and lacks an SSL certificate.") }}</p>
<p id="logs-description">{{ _("View application error log file.") }}</p>
<p id="start-description">{{ _("Start the application process and make app available again on the domain.") }}</p>
<p id="stop-description">{{ _('Stop the application process and make app temporary unavailable.') }}</p>
<p id="restart-description">{{ _('Restart the nodejs application (executes pm2 stop and pm2 start commands).') }}</p>
<p id="uninstall-description">{{ _('Completely remove the nodejs application, including the domain proxy, and records from this manager.') }}</p>
</div>
</div>
<script>
function confirmAppDelete(button) {
var countdown = 5;
var countdownActive = true; // Variable to track countdown status
// Change the button style and text
$(button).removeClass('btn-danger').addClass('btn-dark').html('<i class="bi bi-trash3-fill"></i> Confirm <span class="btn-indicator btn-indicator-mini bg-danger">' + countdown + '</span>');
// Interval to update countdown
var intervalId = setInterval(function () {
countdown--;
// Update the countdown value in the button text
$(button).find('.btn-indicator-mini').text(countdown);
// Remove the onclick event to prevent further changes on subsequent clicks
$(button).removeAttr('onclick');
// If countdown reaches 0, revert the button, clear the interval, and set countdownActive to false
if (countdown === 0) {
clearInterval(intervalId);
revertButton(button);
countdownActive = false;
}
}, 1000);
// Add a click event to the confirm button
$(button).on('click', function () {
// Check if countdown is active before allowing form submission
if (countdownActive) {
// Submit the parent form when the button is clicked during the countdown
const pm2Id = "{{ pm2_data[1] }}";
// Create a new XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.open('POST', `/pm2/delete/${pm2Id}`, true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
// Optionally handle the response
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
// Handle success (e.g., show a success message or refresh the page)
console.log('Deletion successful:', xhr.responseText);
// Optionally remove the deleted item from the DOM or refresh the list
} else {
// Handle error (e.g., show an error message)
console.error('Deletion failed:', xhr.statusText);
}
};
// Send the request
xhr.send();
}
});
}
// Function to revert the button to its initial state
function revertButton(button) {
$(button).removeClass('btn-dark').addClass('btn-danger').html('<i class="bi bi-trash3"></i> Delete');
$(button).attr('onclick', 'confirmAppDelete(this);');
}
</script>
<script>
$('html').on('click', '.view-button', function() {
const button = $(this);
const domain = button.data('file');
fetch(`/pm2/logs/${encodeURIComponent(domain)}`)
.then(response => response.text())
.then(data => {
const modalTitle = $('#viewModalLabel');
const modalBody = $('#viewModal').find('.modal-body');
modalTitle.text("Error log for application: " + domain);
modalBody.empty();
const textContent = document.createElement('pre');
textContent.textContent = data;
modalBody.append(textContent);
$('#viewModal').modal('show');
})
.catch(error => {
console.error('{{ _("Error fetching pm2 logs for applicaiton:") }}', error);
});
});
</script>
<script>
$('html').on('click', '.npm-install-link', function() {
const button = $(this);
var domain = new URLSearchParams(window.location.search).get('domain');
fetch(`/pm2/pip/${encodeURIComponent(domain)}`)
.then(response => response.text())
.then(data => {
const modalTitle = $('#installPIPModalLabel');
const modalBody = $('#installPIPModal').find('.modal-body');
modalTitle.text("Running NPM install for application: " + domain);
modalBody.empty();
const textContent = document.createElement('pre');
textContent.textContent = data;
modalBody.append(textContent);
$('#installPIPModal').modal('show');
})
.catch(error => {
console.error('{{ _("Error running npm install for application:") }}', error);
});
});
</script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
$(document).ready(function() {
// Get the value of the "domain" parameter from the URL
var domainParam = new URLSearchParams(window.location.search).get('domain');
const pipinstallLink = document.getElementById('npm-install-link');
const requirementsstatusElement = document.getElementById('requirements_status');
// Use setTimeout to send the AJAX request 100ms after the page loads
setTimeout(function() {
$.ajax({
type: "GET",
url: "/website/node_info/" + domainParam,
dataType: "json",
success: function(data) {
var nodejsVersion = data.nodejs_version;
var sslStatus = data.ssl_status;
var requirementsFile = data.requirements_file;
var currentDomain = "{{ current_domain }}";
// Check if currentDomain is in punycode format
if (punycode.toASCII(currentDomain) !== currentDomain) {
currentDomain = punycode.toUnicode(currentDomain);
}
if (requirementsFile.includes("exists")) {
requirementsstatusElement.textContent = 'package.json detected';
requirementsstatusElement.classList.remove('bg-dark', 'bg-warning');
requirementsstatusElement.classList.add('bg-success');
pipinstallLink.setAttribute('data-bs-title', `Click to run NPM install from package.json file`);
pipinstallLink.href = `/pm2/npm/${currentDomain}`;
pipinstallLink.target = '_blank';
} else {
requirementsstatusElement.textContent = 'package.json not found';
requirementsstatusElement.classList.remove('bg-dark', 'bg-success');
requirementsstatusElement.classList.add('bg-secondary');
pipinstallLink.setAttribute('data-bs-title', `Add package.json file first to run npm install`);
pipinstallLink.href = `#`;
}
const tooltip = new bootstrap.Tooltip(pipinstallLink);
tooltip.enable();
// Update SSL status display based on the response
var sslButton;
if (sslStatus.includes("VALID")) {
var expiryDate = new Date(sslStatus.split(": ")[1]);
var day = expiryDate.getDate();
var monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
var monthName = monthNames[expiryDate.getMonth()];
var year = expiryDate.getFullYear();
var expiryStatus = day + ' ' + monthName + ' ' + year;
$("#site_link_https").html(`<a href="https://${currentDomain}" target="_blank" class="btn btn-primary d-flex align-items-center gap-2">https://${currentDomain} <i class="bi bi-box-arrow-up-right"></i></a>`);
$("#https_link").html(`<a href="https://${currentDomain}" target="_blank"><img id="screenshot-image" style="width: 700px;" src="/screenshot/{{ current_domain }}" alt="Screenshot of {{ current_domain }}" class="img-fluid"></a>`);
sslButton = `<a href="/ssl" data-bs-toggle="tooltip" data-bs-placement="top" title="{{ _('Expires on:') }} ${expiryStatus}"><span style="color:green;"> {{ _('Valid SSL') }}</span></a>`;
} else {
sslButton = '<a href="/ssl"><span style="color:red;"> {{ _("SSL not detected") }}</span></a>';
}
// Set the HTML content including SSL status and tooltip
$("#site-ssl-status").html(sslButton);
$("#nodejs-version").text(nodejsVersion);
},
error: function(error) {
// Handle any errors here
console.error(error);
}
});
}, 1); // Delay in milliseconds
$('#action_description p').hide();
// Show corresponding description on hover
$('.btn').hover(function() {
var descriptionId = $(this).data('bs-description') + '-description';
//var blankId = '#blank-id';
$('#' + descriptionId).show();
//$(blankId).hide();
}, function() {
var descriptionId = $(this).data('bs-description') + '-description';
//var blankId = '#blank-id';
$('#' + descriptionId).hide();
//$(blankId).show();
});
});
</script>
<hr>
<div class="row mt-3">
{% include 'partials/pagespeed.html' %}
<div class="col-md-9">
<div class="row">
<div class="col-md-2">
<a style="text-decoration: none;" href="/files/{{ current_domain }}" target="_blank"><div class="card" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-title="{{ _('Open application folder in FileManager') }}"">
<div class="card-body ikona text-center">
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="filemanager" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 58 58" style="width:50px; padding-bottom:1em; enable-background:new 0 0 58 58;" xml:space="preserve">
<style type="text/css">
.st0{fill:#BFCCE0;}
.st1{fill:#192030;}
</style>
<path class="st0" d="M52,14H6c-3.3,0-6,2.7000008-6,6v26c0,6.5999985,5.4000001,12,12,12h34c6.5999985,0,12-5.4000015,12-12V20
C58,16.7000008,55.2999992,14,52,14z"/>
<path class="st1" d="M36.0000038,42H21.9999981C20.8999996,42,20,41.1000023,20,40.0000038v-0.0000076
C20,38.8999977,20.8999996,38,21.9999981,38h14.0000057C37.1000023,38,38,38.8999977,38,39.9999962v0.0000076
C38,41.1000023,37.1000023,42,36.0000038,42z"/>
<path class="st1" d="M36.0000038,34H21.9999981C20.8999996,34,20,33.1000023,20,32.0000038v-0.0000057
C20,30.8999996,20.8999996,30,21.9999981,30h14.0000057C37.1000023,30,38,30.8999996,38,31.9999981v0.0000057
C38,33.1000023,37.1000023,34,36.0000038,34z"/>
<path class="st1" d="M6,14h46c0.3412476,0,0.6739502,0.0354614,1,0.0908813V14v-1V6c0-3.2999878-2.7000122-6-6-6H35
c-3.2999878,0-6,2.7000122-6,6v1H11c-3.2999878,0-6,2.7000122-6,6v1.0908813C5.3260498,14.0354614,5.6587524,14,6,14z"/>
</svg>
<h6 class="card-title">{{ _('File Manager') }}</h6>
</div>
</div></a>
</div>
<div class="col-md-2">
<a style="text-decoration: none;" id="pm2link" href="/pm2">
<div class="card" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-title="{{ _('Go to Applications Manager') }}">
<div class="card-body ikona text-center">
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="DB" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 58 58" style="width:50px; padding-bottom:1em; enable-background:new 0 0 58 58;" xml:space="preserve">
<style type="text/css">
.st0{fill:#BFCCE0;}
.st1{fill:#192030;}
</style>
<path class="st0" d="M52,16H6c-3.3,0-6-2.6999998-6-6V6c0-3.3,2.7-6,6-6h46c3.2999992,0,6,2.7,6,6v4
C58,13.3000002,55.2999992,16,52,16z"/>
<path class="st1" d="M29.0000019,10H16.9999981C15.8999987,10,15,9.1000013,15,8.0000019V7.9999981
C15,6.8999991,15.8999987,6,16.9999981,6h12.0000038C30.1000004,6,31,6.8999991,31,7.9999981v0.0000038
C31,9.1000013,30.1000004,10,29.0000019,10z"/>
<circle class="st1" cx="8" cy="8" r="2"/>
<path class="st0" d="M52,37H6c-3.3,0-6-2.7000008-6-6v-4c0-3.2999992,2.7-6,6-6h46c3.2999992,0,6,2.7000008,6,6v4
C58,34.2999992,55.2999992,37,52,37z"/>
<path class="st1" d="M29.0000019,31H16.9999981C15.8999987,31,15,30.1000004,15,29.0000019v-0.0000038
C15,27.8999996,15.8999987,27,16.9999981,27h12.0000038C30.1000004,27,31,27.8999996,31,28.9999981v0.0000038
C31,30.1000004,30.1000004,31,29.0000019,31z"/>
<circle class="st1" cx="8" cy="29" r="2"/>
<path class="st0" d="M52,58H6c-3.3,0-6-2.7000008-6-6v-4c0-3.2999992,2.7-6,6-6h46c3.2999992,0,6,2.7000008,6,6v4
C58,55.2999992,55.2999992,58,52,58z"/>
<path class="st1" d="M29.0000019,52H16.9999981C15.8999987,52,15,51.1000023,15,50.0000038v-0.0000076
C15,48.8999977,15.8999987,48,16.9999981,48h12.0000038C30.1000004,48,31,48.8999977,31,49.9999962v0.0000076
C31,51.1000023,30.1000004,52,29.0000019,52z"/>
<circle class="st1" cx="8" cy="50" r="2"/>
</svg>
<h6 class="card-title">{{ _('PM2') }}</h6>
</div>
</div></a>
</div>
<div class="col-md-2">
<a style="text-decoration: none;" id="npm-install-link" href="/npm-install" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-title="{{ _('Install dependencies from package.json file') }}">
<div class="card">
<div class="card-body ikona text-center">
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="pip" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 58 58" style="width:50px; padding-bottom:1em; enable-background:new 0 0 58 58;" xml:space="preserve">
<style type="text/css">
.st0{fill:#BFCCE0;}
.st1{fill:#192030;}
</style><path class="st0" d="M0,18v22c0,9.9,8.1,18,18,18h22c9.9,0,18-8.1,18-18V18c0-2.9-0.7-5.6-1.9-8H1.9C0.7,12.4,0,15.1,0,18z"/> <path class="st1" d="M40,0H18C11,0,4.9,4.1,1.9,10h54.2C53.1,4.1,47,0,40,0z"/> <g> <path class="st1" d="M27,45c-0.1,0-0.3,0-0.4,0c-1.1-0.2-1.8-1.3-1.6-2.3l4-21c0.2-1.1,1.3-1.8,2.3-1.6c1.1,0.2,1.8,1.3,1.6,2.3 l-4,21C28.8,44.3,27.9,45,27,45z"/> </g> <g> <path class="st1" d="M18,41c-0.6,0-1.1-0.2-1.5-0.7l-6-7c-0.7-0.8-0.6-2.1,0.2-2.8c0.8-0.7,2.1-0.6,2.8,0.2l6,7 c0.7,0.8,0.6,2.1-0.2,2.8C18.9,40.8,18.5,41,18,41z"/> </g> <g> <path class="st1" d="M12,34c-0.5,0-0.9-0.2-1.3-0.5c-0.8-0.7-0.9-2-0.2-2.8l6-7c0.7-0.8,2-0.9,2.8-0.2c0.8,0.7,0.9,2,0.2,2.8l-6,7 C13.1,33.8,12.6,34,12,34z"/> </g> <g> <path class="st1" d="M40,41c-0.5,0-0.9-0.2-1.3-0.5c-0.8-0.7-0.9-2-0.2-2.8l6-7c0.7-0.8,2-0.9,2.8-0.2c0.8,0.7,0.9,2,0.2,2.8l-6,7 C41.1,40.8,40.6,41,40,41z"/> </g> <g> <path class="st1" d="M46,34c-0.6,0-1.1-0.2-1.5-0.7l-6-7c-0.7-0.8-0.6-2.1,0.2-2.8c0.8-0.7,2.1-0.6,2.8,0.2l6,7 c0.7,0.8,0.6,2.1-0.2,2.8C46.9,33.8,46.5,34,46,34z"/> </g> </svg>
<h6 class="card-title" id="manage_phpmyadmin_public_title">{{ _('Run NPM Install') }}<br><span id="requirements_status" class="badge bg-dark">{{ _('Checking...') }}</span></h6>
</div>
</div></a>
</div>
<div class="col-md-2">
<div class="card" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-title="{{ _('View SSL Certificates') }}">
<a style="text-decoration: none;" href="/ssl" target="_blank">
<div class="card-body ikona text-center">
<svg version="1.1" id="katanac" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 58 58" style="width:50px; padding-bottom:1em; enable-background:new 0 0 58 58;" xml:space="preserve">
<style type="text/css">
.st0{fill:#BFCCE0;}
.st1{fill:#192030;}
</style>
<path class="st0" d="M38,58H12C5.4,58,0,52.6,0,46V32c0-6.6,5.4-12,12-12h26c6.6,0,12,5.4,12,12v14C50,52.6,44.6,58,38,58z"/>
<path class="st1" d="M25,46L25,46c-1.1,0-2-0.9-2-2V34c0-1.1,0.9-2,2-2h0c1.1,0,2,0.9,2,2v10C27,45.1,26.1,46,25,46z"/>
<path class="st1" d="M12,20h1v-6c0-5.5,4.5-10,10-10h4c5.5,0,10,4.5,10,10v6h1c1,0,2,0.1,3,0.4V14c0-7.7-6.3-14-14-14h-4
C15.3,0,9,6.3,9,14v6.4C10,20.1,11,20,12,20z"/>
</svg>
<h6 class="card-title">
<span id="site-ssl-status">
<div class="spinner-border spinner-border-sm" role="status"></div>{{ _('Checking SSL status..') }}</span>
</h6>
</div></a>
</div>
</div>
<div class="col-md-2">
<a style="text-decoration: none;" href="/cronjobs" target="_blank"> <div class="card" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-title="{{ _('Manage cronjobs') }}">
<div class="card-body ikona text-center">
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="cronovi" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 54 58" style="width:50px; padding-bottom:1em; enable-background:new 0 0 54 58;" xml:space="preserve">
<style type="text/css">
.st0{fill:#BFCCE0;}
.st1{fill:#192030;}
</style>
<path class="st0" d="M42,58H12C5.4000001,58,0,52.5999985,0,46V21h54v25C54,52.5999985,48.5999985,58,42,58z"/>
<path class="st1" d="M54,21H0v-3C0,11.3999996,5.4000001,6,12,6h30c6.5999985,0,12,5.3999996,12,12V21z"/>
<path class="st0" d="M13.0000019,12h-0.0000038C11.8999987,12,11,11.1000013,11,10.0000019V1.999998
C11,0.8999991,11.8999987,0,12.9999981,0h0.0000038C14.1000013,0,15,0.8999991,15,1.999998v8.0000038
C15,11.1000013,14.1000013,12,13.0000019,12z"/>
<path class="st0" d="M41.0000038,12h-0.0000076C39.8999977,12,39,11.1000013,39,10.0000019V1.999998
C39,0.8999991,39.8999977,0,40.9999962,0h0.0000076C42.1000023,0,43,0.8999991,43,1.999998v8.0000038
C43,11.1000013,42.1000023,12,41.0000038,12z"/>
<path class="st1" d="M28.0000019,35h-2.0000038C24.8999996,35,24,34.1000023,24,33.0000038v-0.0000076
C24,31.8999996,24.8999996,31,25.9999981,31h2.0000038C29.1000004,31,30,31.8999996,30,32.9999962v0.0000076
C30,34.1000023,29.1000004,35,28.0000019,35z"/>
<path class="st1" d="M15.0000019,35h-2.0000038C11.8999987,35,11,34.1000023,11,33.0000038v-0.0000076
C11,31.8999996,11.8999987,31,12.9999981,31h2.0000038C16.1000004,31,17,31.8999996,17,32.9999962v0.0000076
C17,34.1000023,16.1000004,35,15.0000019,35z"/>
<path class="st1" d="M41.0000038,35h-2.0000076C37.8999977,35,37,34.1000023,37,33.0000038v-0.0000076
C37,31.8999996,37.8999977,31,38.9999962,31h2.0000076C42.1000023,31,43,31.8999996,43,32.9999962v0.0000076
C43,34.1000023,42.1000023,35,41.0000038,35z"/>
<path class="st1" d="M28.0000019,47h-2.0000038C24.8999996,47,24,46.1000023,24,45.0000038v-0.0000076
C24,43.8999977,24.8999996,43,25.9999981,43h2.0000038C29.1000004,43,30,43.8999977,30,44.9999962v0.0000076
C30,46.1000023,29.1000004,47,28.0000019,47z"/>
<path class="st1" d="M15.0000019,47h-2.0000038C11.8999987,47,11,46.1000023,11,45.0000038v-0.0000076
C11,43.8999977,11.8999987,43,12.9999981,43h2.0000038C16.1000004,43,17,43.8999977,17,44.9999962v0.0000076
C17,46.1000023,16.1000004,47,15.0000019,47z"/>
<path class="st1" d="M41.0000038,47h-2.0000076C37.8999977,47,37,46.1000023,37,45.0000038v-0.0000076
C37,43.8999977,37.8999977,43,38.9999962,43h2.0000076C42.1000023,43,43,43.8999977,43,44.9999962v0.0000076
C43,46.1000023,42.1000023,47,41.0000038,47z"/>
</svg>
<h6 class="card-title">{{ _('Cronjobs') }}</h6>
</div>
</div></a>
</div>
{% if 'malware_scan' in enabled_modules %}
<div class="col-md-2">
<div class="card">
<a style="text-decoration: none;" href="/malware-scanner?path=/home/{{current_username}}/{{ current_domain }}" target="_blank" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-title="Scan website files with ClamAV">
<div class="card-body ikona text-center">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="scanner" x="0px" y="0px" viewBox="0 0 58 58" style="width:50px; padding-bottom:1em; enable-background:new 0 0 58 58;" xml:space="preserve"> <style type="text/css"> .st0{fill:#414954;} .st1{fill:#EBF3FF;} </style> <g> <path class="st0" d="M12,37v1c0,5.5,4.5,10,10,10h14c5.5,0,10-4.5,10-10v-1H12z"/> <path class="st0" d="M46,27v-7c0-5.5-4.5-10-10-10H22c-5.5,0-10,4.5-10,10v7H46z"/> </g> <g> <path class="st1" d="M2,14c-1.1,0-2-0.9-2-2C0,5.4,5.4,0,12,0c1.1,0,2,0.9,2,2s-0.9,2-2,2c-4.4,0-8,3.6-8,8C4,13.1,3.1,14,2,14z"/> </g> <g> <path class="st1" d="M12,58C5.4,58,0,52.6,0,46c0-1.1,0.9-2,2-2s2,0.9,2,2c0,4.4,3.6,8,8,8c1.1,0,2,0.9,2,2S13.1,58,12,58z"/> </g> <g> <path class="st1" d="M46,58c-1.1,0-2-0.9-2-2s0.9-2,2-2c4.4,0,8-3.6,8-8c0-1.1,0.9-2,2-2s2,0.9,2,2C58,52.6,52.6,58,46,58z"/> </g> <g> <path class="st1" d="M56,14c-1.1,0-2-0.9-2-2c0-4.4-3.6-8-8-8c-1.1,0-2-0.9-2-2s0.9-2,2-2c6.6,0,12,5.4,12,12 C58,13.1,57.1,14,56,14z"/> </g> <path class="st1" d="M56,34H2c-1.1,0-2-0.9-2-2v0c0-1.1,0.9-2,2-2h54c1.1,0,2,0.9,2,2v0C58,33.1,57.1,34,56,34z"/> </svg>
<h6 class="card-title">{{ _('Scanning') }}</h6>
</div>
</a>
</div>
</div>
{% endif %}
</div>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", function() {
var punycodeElements = document.querySelectorAll(".punycode");
punycodeElements.forEach(function(element) {
element.textContent = punycode.toUnicode(element.textContent);
});
});
document.addEventListener('DOMContentLoaded', function() {
const domainElement = document.querySelector('.card-text .punycode.nije-link');
const faviconElement = document.getElementById('favicon');
const currentDomain = '{{ current_domain }}';
// Create the URL to the favicon
const faviconUrl = `https://www.google.com/s2/favicons?domain=${currentDomain}`;
// Create an img element and set its src to the favicon URL
const img = document.createElement('img');
img.src = faviconUrl;
img.alt = 'Favicon';
img.style.width = '16px'; // Optional: set the size of the favicon
img.style.height = '16px'; // Optional: set the size of the favicon
// Append the img element to the favicon span
faviconElement.appendChild(img);
});
</script>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,863 @@
<!-- manager/python.html -->
{% extends 'base.html' %}
{% block content %}
<div class="modal fade" id="installPIPModal" tabindex="-1" role="dialog" aria-labelledby="installPIPModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="installPIPModalLabel">{{ _('Application Logs') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ _('Close') }}"></button>
</div>
<div class="modal-body bg-dark">
<div id="pipinstalllogprogress" class="mb-0"></div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="viewModal" tabindex="-1" role="dialog" aria-labelledby="viewModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="viewModalLabel">{{ _('Application Logs') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ _('Close') }}"></button>
</div>
<div class="modal-body bg-dark">
<div id="fileContent" class="mb-0"></div>
</div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/punycode/2.1.1/punycode.min.js"></script>
<style>
.st1 {
fill: #192030;
}
/* Style for dark mode svg icons */
[data-skin="dark"] .st1 {
fill: #506fd9;
}
</style>
<style>
.nije-link {
text-decoration: none;
color: black;
border-bottom: 1px dashed black;
}
.ikona:hover {
background: aliceblue;
}
#action_description p {
display: none;
margin-bottom: 0px;
margin-top: 1rem;
}
</style>
{% if current_domain %}
<div class="card">
<div class="row">
<div class="col-md-auto">
{% include 'partials/screenshot.html' %}
</div>
<div class="col">
<a href="#" onclick="updateScreenshot()" id="refresh_site_screenshot">
<i class="bi bi-arrow-clockwise" style="left: 15px;top: 15px;position: absolute; padding: 6px 10px;" data-bs-toggle="tooltip" data-bs-placement="right" data-bs-title="Refresh website screenshot"></i>
</a>
<div class="row">
<div class="col">
<div class="card-body">
<small class="card-text">Status</small>
<p class="card-text {% if pm2_data[8] == 'stopped' %}text-danger{% elif pm2_data[8] == 'online' %}text-success{% else %}text-warning{% endif %}">● {{ pm2_data[8] }}</p>
</div>
</div>
<div class="col">
<div class="card-body">
<small class="card-text">{{ _('Type') }}</small>
<p class="card-text"><i class=""><img src="/static/images/icons/python.png" style="height: 1em;"></i> {{ container.type }}</p>
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="card-body">
<small class="card-text">{{ _('Files') }} <span id="filesSize">({{ _('Calculating size...') }})</span></small>
<p class="card-text"><a class="nije-link" href="/files/{{ current_domain }}">{{ domain_directory }}</a></p>
</div>
</div>
<div class="col">
<div class="card-body">
<small class="card-text">{{ _('Domain:') }}</small>
<p class="card-text">
<span id="favicon"></span> <a class="punycode nije-link" href="/domains">
{{ current_domain }}
</a>
</p>
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="card-body">
<small class="card-text">
<a class="nije-link" data-bs-toggle="collapse" href="#collapseAppInfo" aria-expanded="false" aria-controls="collapseAppInfo">{{ _('Status Information') }}</a>
</small>
<p class="card-text">
<div class="collapse" id="collapseAppInfo">
<ul class="list-group">
<li class="list-group-item"><small>{{ _('Uptime:') }}</small> <span><b>{{ pm2_data[6] }}</b></span></li>
<li class="list-group-item"><small>{{ _('Restarts:') }}</small> <span><b>{{ pm2_data[7] }}</b></span></li>
<li class="list-group-item"><small>{{ _('Watching:') }}</small> <span><b>{{ pm2_data[12] }}</b></span></li>
</ul>
</div>
</div>
</div>
<div class="col">
<div class="card-body">
<small class="card-text">{{ _('Created') }} <a data-bs-toggle="tooltip" data-bs-placement="right" data-bs-title="{{ container.created_date }}"><i class="bi bi-info-circle"></i></a></small>
</button>
<p class="card-text">
<span id="created_date">{{ container.created_date }}</span>
</p>
<script>
function GetFIlesSize() {
const filesSizeSpan = document.getElementById("filesSize");
const currentDomain = "{{current_domain}}";
// Fetch files size
fetch(`/get-files-size?selected_domain=${encodeURIComponent(currentDomain)}`)
.then(response => response.json())
.then(data => {
filesSizeSpan.textContent = "(" + data.size + ")";
})
.catch(error => {
console.error("An error occurred:", error);
});
}
GetFIlesSize()
</script>
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="card-body">
<small class="card-text">{{ _('Python version:') }}</small>
<h4 class="card-text"><span id="python-version"></span></h4>
</div>
</div><div class="col">
<div class="card-body">
<small class="card-text">{{ _('CPU usage:') }}</small>
<h4 class="card-text"><i class="dashboard_icon bi bi-cpu"></i> {{ pm2_data[9] }}</h4>
</div>
</div>
<div class="col">
<div class="card-body">
<small class="card-text">{{ _('Memory usage:') }}</small>
<h4 class="card-text"><i class="dashboard_icon bi bi-memory"></i> {{ pm2_data[10] }}</span></h4>
</div>
</div>
</div>
</div>
</div>
</div>
<!--p>domain id: {{ container.domain_id }}</p-->
<div class="row my-4">
<div role="group" aria-label="WP Actions" style="width: 100%; margin-top: 0px;" class="btn-group">
{% if 'temporary_links' in enabled_modules %}
<button type="button" class="btn btn-outline-dark btn-lg" data-bs-description="preview" data-selected-domain="{{ current_domain }}" data-bs-toggle="modal" data-bs-target="#previewModal" onclick="sendDataToPreview(event)"><span class="desktop-only">{{ _('Preview') }}</span><span class="mobile-only"><i class="bi bi-box-arrow-up-right"></i></span></button>
<script>
function sendDataToPreview(event) {
const button = event.currentTarget;
const domain = button.getAttribute('data-selected-domain');
fetch(`/domains/temporary-link?domain=${encodeURIComponent(domain)}`)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
if (data.link) {
window.open(data.link, '_blank');
} else {
console.error('No link found in the response.');
}
})
.catch(error => {
console.error('Error:', error);
});
}
</script>
{% endif %}
<button type="button" class="btn btn-outline-primary view-button btn-lg" data-file="{{ pm2_data[1] }}" data-type="logs" data-bs-description="logs">
<span class="desktop-only">{{ _('View Error Log') }}</span><span class="mobile-only"><i class="bi bi-bug"></i></span>
</button>
{% if pm2_data[8] == 'stopped' %}
<button type="button" class="btn btn-outline-success btn-lg" data-bs-description="start" onclick="startApp(this);">
<i class="bi bi-play-fill"></i><span class="desktop-only"> {{ _('Start') }}</span>
</button>
<script>
function startApp(button) {
// Get the PM2 process ID
const pm2Id = "{{ pm2_data[1] }}";
// Create a new XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.open('POST', `/pm2/start/${pm2Id}`, true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
// Optionally handle the response
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
// Handle success (e.g., show a success message or refresh the page)
console.log('Start successful:', xhr.responseText);
// Optionally refresh the application list or update the UI
} else {
// Handle error (e.g., show an error message)
console.error('Start failed:', xhr.statusText);
}
};
// Send the request
xhr.send();
}
</script>
{% else %}
<button type="button" class="btn btn-outline-danger btn-lg" data-bs-description="stop" onclick="stopApp(this);">
<i class="bi bi-stop-fill"></i><span class="desktop-only"> {{ _('Stop') }}</span>
</button>
<script>
function stopApp(button) {
// Get the PM2 process ID
const pm2Id = "{{ pm2_data[1] }}";
// Create a new XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.open('POST', `/pm2/stop/${pm2Id}`, true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
// Optionally handle the response
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
// Handle success (e.g., show a success message or refresh the page)
console.log('Stop successful:', xhr.responseText);
// Optionally refresh the application list or update the UI
} else {
// Handle error (e.g., show an error message)
console.error('Stop failed:', xhr.statusText);
}
};
// Send the request
xhr.send();
}
</script>
{% endif %}
<button type="button" data-bs-description="restart" id="restartButton" class="btn btn-primary btn-lg"></i><i class="bi bi-arrow-clockwise"></i><span class="desktop-only"> {{ _('Restart') }}</span></button>
<script>
document.getElementById('restartButton').addEventListener('click', function(event) {
event.preventDefault(); // Prevent the default button action
// Get the PM2 process ID
const pm2Id = "{{ pm2_data[1] }}";
// Create a new XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.open('POST', `/pm2/restart/${pm2Id}`, true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
// Optionally handle the response
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
// Handle success (e.g., show a success message)
console.log('Restart successful:', xhr.responseText);
} else {
// Handle error (e.g., show an error message)
console.error('Restart failed:', xhr.statusText);
}
};
// Send the request
xhr.send();
});
</script>
<button type="button" class="btn btn-danger btn-lg" data-bs-description="uninstall" onclick="confirmAppDelete(this);"><i class="bi bi-trash3"></i><span class="desktop-only"> {{ _('Uninstall') }}</span></button>
</div>
<div id="action_description" class="">
<p id="blank-id">&nbsp;</p>
<p id="preview-description">{{ _("Preview website with a temporary domain,") }} <b>{{ _("valid for 15 minutes only!") }}</b> {{ _("Helpful if your domain hasn't been pointed to the server's IP address yet and lacks an SSL certificate.") }}</p>
<p id="logs-description">{{ _("View application error log file.") }}</p>
<p id="start-description">{{ _("Start the application process and make app available again on the domain.") }}</p>
<p id="stop-description">{{ _('Stop the application process and make app temporary unavailable.') }}</p>
<p id="restart-description">{{ _('Restart the python application (executes pm2 stop and pm2 start commands).') }}</p>
<p id="uninstall-description">{{ _('Completely remove the python application, including the domain proxy, and records from this manager.') }}</p>
</div>
</div>
<script>
function confirmAppDelete(button) {
var countdown = 5;
var countdownActive = true; // Variable to track countdown status
// Change the button style and text
$(button).removeClass('btn-danger').addClass('btn-dark').html('<i class="bi bi-trash3-fill"></i> Confirm <span class="btn-indicator btn-indicator-mini bg-danger">' + countdown + '</span>');
// Interval to update countdown
var intervalId = setInterval(function () {
countdown--;
// Update the countdown value in the button text
$(button).find('.btn-indicator-mini').text(countdown);
// Remove the onclick event to prevent further changes on subsequent clicks
$(button).removeAttr('onclick');
// If countdown reaches 0, revert the button, clear the interval, and set countdownActive to false
if (countdown === 0) {
clearInterval(intervalId);
revertButton(button);
countdownActive = false;
}
}, 1000);
// Add a click event to the confirm button
$(button).on('click', function () {
// Check if countdown is active before allowing form submission
if (countdownActive) {
// Submit the parent form when the button is clicked during the countdown
const pm2Id = "{{ pm2_data[1] }}";
// Create a new XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.open('POST', `/pm2/delete/${pm2Id}`, true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
// Optionally handle the response
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
// Handle success (e.g., show a success message or refresh the page)
console.log('Deletion successful:', xhr.responseText);
// Optionally remove the deleted item from the DOM or refresh the list
} else {
// Handle error (e.g., show an error message)
console.error('Deletion failed:', xhr.statusText);
}
};
// Send the request
xhr.send();
}
});
}
// Function to revert the button to its initial state
function revertButton(button) {
$(button).removeClass('btn-dark').addClass('btn-danger').html('<i class="bi bi-trash3"></i> Delete');
$(button).attr('onclick', 'confirmAppDelete(this);');
}
</script>
<script>
$('html').on('click', '.view-button', function() {
const button = $(this);
const domain = button.data('file');
fetch(`/pm2/logs/${encodeURIComponent(domain)}`)
.then(response => response.text())
.then(data => {
const modalTitle = $('#viewModalLabel');
const modalBody = $('#viewModal').find('.modal-body');
modalTitle.text("Error log for application: " + domain);
modalBody.empty();
const textContent = document.createElement('pre');
textContent.textContent = data;
modalBody.append(textContent);
$('#viewModal').modal('show');
})
.catch(error => {
console.error('{{ _("Error fetching pm2 logs for applicaiton:") }}', error);
});
});
</script>
<script>
$('html').on('click', '.pip-install-link', function() {
const button = $(this);
var domain = new URLSearchParams(window.location.search).get('domain');
fetch(`/pm2/pip/${encodeURIComponent(domain)}`)
.then(response => response.text())
.then(data => {
const modalTitle = $('#installPIPModalLabel');
const modalBody = $('#installPIPModal').find('.modal-body');
modalTitle.text("Running pip install for application: " + domain);
modalBody.empty();
const textContent = document.createElement('pre');
textContent.textContent = data;
modalBody.append(textContent);
$('#installPIPModal').modal('show');
})
.catch(error => {
console.error('{{ _("Error running pip install for application:") }}', error);
});
});
</script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
$(document).ready(function() {
// Get the value of the "domain" parameter from the URL
var domainParam = new URLSearchParams(window.location.search).get('domain');
const pipinstallLink = document.getElementById('pip-install-link');
const requirementsstatusElement = document.getElementById('requirements_status');
// Use setTimeout to send the AJAX request 100ms after the page loads
setTimeout(function() {
$.ajax({
type: "GET",
url: "/website/py_info/" + domainParam,
dataType: "json",
success: function(data) {
var pythonVersion = data.python_version;
var sslStatus = data.ssl_status;
var requirementsFile = data.requirements_file;
var currentDomain = "{{ current_domain }}";
// Check if currentDomain is in punycode format
if (punycode.toASCII(currentDomain) !== currentDomain) {
currentDomain = punycode.toUnicode(currentDomain);
}
if (requirementsFile.includes("exists")) {
requirementsstatusElement.textContent = 'requirements.txt detected';
requirementsstatusElement.classList.remove('bg-dark', 'bg-warning');
requirementsstatusElement.classList.add('bg-success');
pipinstallLink.setAttribute('data-bs-title', `Click to run PIP install from requirements.txt file`);
pipinstallLink.href = `/pm2/pip/${currentDomain}`;
pipinstallLink.target = '_blank';
} else {
requirementsstatusElement.textContent = 'requirements.txt not found';
requirementsstatusElement.classList.remove('bg-dark', 'bg-success');
requirementsstatusElement.classList.add('bg-secondary');
pipinstallLink.setAttribute('data-bs-title', `Add requirements.txt file first to run pip install`);
pipinstallLink.href = `#`;
}
const tooltip = new bootstrap.Tooltip(pipinstallLink);
tooltip.enable();
// Update SSL status display based on the response
var sslButton;
if (sslStatus.includes("VALID")) {
var expiryDate = new Date(sslStatus.split(": ")[1]);
var day = expiryDate.getDate();
var monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
var monthName = monthNames[expiryDate.getMonth()];
var year = expiryDate.getFullYear();
var expiryStatus = day + ' ' + monthName + ' ' + year;
$("#site_link_https").html(`<a href="https://${currentDomain}" target="_blank" class="btn btn-primary d-flex align-items-center gap-2">https://${currentDomain} <i class="bi bi-box-arrow-up-right"></i></a>`);
$("#https_link").html(`<a href="https://${currentDomain}" target="_blank"><img id="screenshot-image" style="width: 700px;" src="/screenshot/{{ current_domain }}" alt="Screenshot of {{ current_domain }}" class="img-fluid"></a>`);
sslButton = `<a href="/ssl" data-bs-toggle="tooltip" data-bs-placement="top" title="{{ _('Expires on:') }} ${expiryStatus}"><span style="color:green;"> {{ _('Valid SSL') }}</span></a>`;
} else {
sslButton = '<a href="/ssl"><span style="color:red;"> {{ _("SSL not detected") }}</span></a>';
}
// Set the HTML content including SSL status and tooltip
$("#site-ssl-status").html(sslButton);
$("#python-version").text(pythonVersion);
},
error: function(error) {
// Handle any errors here
console.error(error);
}
});
}, 1); // Delay in milliseconds
$('#action_description p').hide();
// Show corresponding description on hover
$('.btn').hover(function() {
var descriptionId = $(this).data('bs-description') + '-description';
//var blankId = '#blank-id';
$('#' + descriptionId).show();
//$(blankId).hide();
}, function() {
var descriptionId = $(this).data('bs-description') + '-description';
//var blankId = '#blank-id';
$('#' + descriptionId).hide();
//$(blankId).show();
});
});
</script>
<hr>
<div class="row mt-3">
{% include 'partials/pagespeed.html' %}
<div class="col-md-9">
<div class="row">
<div class="col-md-2">
<a style="text-decoration: none;" href="/files/{{ current_domain }}" target="_blank"><div class="card" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-title="{{ _('Open application folder in FileManager') }}"">
<div class="card-body ikona text-center">
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="filemanager" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 58 58" style="width:50px; padding-bottom:1em; enable-background:new 0 0 58 58;" xml:space="preserve">
<style type="text/css">
.st0{fill:#BFCCE0;}
.st1{fill:#192030;}
</style>
<path class="st0" d="M52,14H6c-3.3,0-6,2.7000008-6,6v26c0,6.5999985,5.4000001,12,12,12h34c6.5999985,0,12-5.4000015,12-12V20
C58,16.7000008,55.2999992,14,52,14z"/>
<path class="st1" d="M36.0000038,42H21.9999981C20.8999996,42,20,41.1000023,20,40.0000038v-0.0000076
C20,38.8999977,20.8999996,38,21.9999981,38h14.0000057C37.1000023,38,38,38.8999977,38,39.9999962v0.0000076
C38,41.1000023,37.1000023,42,36.0000038,42z"/>
<path class="st1" d="M36.0000038,34H21.9999981C20.8999996,34,20,33.1000023,20,32.0000038v-0.0000057
C20,30.8999996,20.8999996,30,21.9999981,30h14.0000057C37.1000023,30,38,30.8999996,38,31.9999981v0.0000057
C38,33.1000023,37.1000023,34,36.0000038,34z"/>
<path class="st1" d="M6,14h46c0.3412476,0,0.6739502,0.0354614,1,0.0908813V14v-1V6c0-3.2999878-2.7000122-6-6-6H35
c-3.2999878,0-6,2.7000122-6,6v1H11c-3.2999878,0-6,2.7000122-6,6v1.0908813C5.3260498,14.0354614,5.6587524,14,6,14z"/>
</svg>
<h6 class="card-title">{{ _('File Manager') }}</h6>
</div>
</div></a>
</div>
<div class="col-md-2">
<a style="text-decoration: none;" id="pm2link" href="/pm2">
<div class="card" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-title="{{ _('Go to Applications Manager') }}">
<div class="card-body ikona text-center">
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="DB" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 58 58" style="width:50px; padding-bottom:1em; enable-background:new 0 0 58 58;" xml:space="preserve">
<style type="text/css">
.st0{fill:#BFCCE0;}
.st1{fill:#192030;}
</style>
<path class="st0" d="M52,16H6c-3.3,0-6-2.6999998-6-6V6c0-3.3,2.7-6,6-6h46c3.2999992,0,6,2.7,6,6v4
C58,13.3000002,55.2999992,16,52,16z"/>
<path class="st1" d="M29.0000019,10H16.9999981C15.8999987,10,15,9.1000013,15,8.0000019V7.9999981
C15,6.8999991,15.8999987,6,16.9999981,6h12.0000038C30.1000004,6,31,6.8999991,31,7.9999981v0.0000038
C31,9.1000013,30.1000004,10,29.0000019,10z"/>
<circle class="st1" cx="8" cy="8" r="2"/>
<path class="st0" d="M52,37H6c-3.3,0-6-2.7000008-6-6v-4c0-3.2999992,2.7-6,6-6h46c3.2999992,0,6,2.7000008,6,6v4
C58,34.2999992,55.2999992,37,52,37z"/>
<path class="st1" d="M29.0000019,31H16.9999981C15.8999987,31,15,30.1000004,15,29.0000019v-0.0000038
C15,27.8999996,15.8999987,27,16.9999981,27h12.0000038C30.1000004,27,31,27.8999996,31,28.9999981v0.0000038
C31,30.1000004,30.1000004,31,29.0000019,31z"/>
<circle class="st1" cx="8" cy="29" r="2"/>
<path class="st0" d="M52,58H6c-3.3,0-6-2.7000008-6-6v-4c0-3.2999992,2.7-6,6-6h46c3.2999992,0,6,2.7000008,6,6v4
C58,55.2999992,55.2999992,58,52,58z"/>
<path class="st1" d="M29.0000019,52H16.9999981C15.8999987,52,15,51.1000023,15,50.0000038v-0.0000076
C15,48.8999977,15.8999987,48,16.9999981,48h12.0000038C30.1000004,48,31,48.8999977,31,49.9999962v0.0000076
C31,51.1000023,30.1000004,52,29.0000019,52z"/>
<circle class="st1" cx="8" cy="50" r="2"/>
</svg>
<h6 class="card-title">{{ _('PM2') }}</h6>
</div>
</div></a>
</div>
<div class="col-md-2">
<a style="text-decoration: none;" id="pip-install-link" href="/pip-install" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-title="{{ _('Install dependencies from requirements.txt file') }}">
<div class="card">
<div class="card-body ikona text-center">
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="pip" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 58 58" style="width:50px; padding-bottom:1em; enable-background:new 0 0 58 58;" xml:space="preserve">
<style type="text/css">
.st0{fill:#BFCCE0;}
.st1{fill:#192030;}
</style><path class="st0" d="M0,18v22c0,9.9,8.1,18,18,18h22c9.9,0,18-8.1,18-18V18c0-2.9-0.7-5.6-1.9-8H1.9C0.7,12.4,0,15.1,0,18z"/> <path class="st1" d="M40,0H18C11,0,4.9,4.1,1.9,10h54.2C53.1,4.1,47,0,40,0z"/> <g> <path class="st1" d="M27,45c-0.1,0-0.3,0-0.4,0c-1.1-0.2-1.8-1.3-1.6-2.3l4-21c0.2-1.1,1.3-1.8,2.3-1.6c1.1,0.2,1.8,1.3,1.6,2.3 l-4,21C28.8,44.3,27.9,45,27,45z"/> </g> <g> <path class="st1" d="M18,41c-0.6,0-1.1-0.2-1.5-0.7l-6-7c-0.7-0.8-0.6-2.1,0.2-2.8c0.8-0.7,2.1-0.6,2.8,0.2l6,7 c0.7,0.8,0.6,2.1-0.2,2.8C18.9,40.8,18.5,41,18,41z"/> </g> <g> <path class="st1" d="M12,34c-0.5,0-0.9-0.2-1.3-0.5c-0.8-0.7-0.9-2-0.2-2.8l6-7c0.7-0.8,2-0.9,2.8-0.2c0.8,0.7,0.9,2,0.2,2.8l-6,7 C13.1,33.8,12.6,34,12,34z"/> </g> <g> <path class="st1" d="M40,41c-0.5,0-0.9-0.2-1.3-0.5c-0.8-0.7-0.9-2-0.2-2.8l6-7c0.7-0.8,2-0.9,2.8-0.2c0.8,0.7,0.9,2,0.2,2.8l-6,7 C41.1,40.8,40.6,41,40,41z"/> </g> <g> <path class="st1" d="M46,34c-0.6,0-1.1-0.2-1.5-0.7l-6-7c-0.7-0.8-0.6-2.1,0.2-2.8c0.8-0.7,2.1-0.6,2.8,0.2l6,7 c0.7,0.8,0.6,2.1-0.2,2.8C46.9,33.8,46.5,34,46,34z"/> </g> </svg>
<h6 class="card-title" id="manage_phpmyadmin_public_title">{{ _('Run PIP Install') }}<br><span id="requirements_status" class="badge bg-dark">{{ _('Checking...') }}</span></h6>
</div>
</div></a>
</div>
<div class="col-md-2">
<div class="card" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-title="{{ _('View SSL Certificates') }}">
<a style="text-decoration: none;" href="/ssl" target="_blank">
<div class="card-body ikona text-center">
<svg version="1.1" id="katanac" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 58 58" style="width:50px; padding-bottom:1em; enable-background:new 0 0 58 58;" xml:space="preserve">
<style type="text/css">
.st0{fill:#BFCCE0;}
.st1{fill:#192030;}
</style>
<path class="st0" d="M38,58H12C5.4,58,0,52.6,0,46V32c0-6.6,5.4-12,12-12h26c6.6,0,12,5.4,12,12v14C50,52.6,44.6,58,38,58z"/>
<path class="st1" d="M25,46L25,46c-1.1,0-2-0.9-2-2V34c0-1.1,0.9-2,2-2h0c1.1,0,2,0.9,2,2v10C27,45.1,26.1,46,25,46z"/>
<path class="st1" d="M12,20h1v-6c0-5.5,4.5-10,10-10h4c5.5,0,10,4.5,10,10v6h1c1,0,2,0.1,3,0.4V14c0-7.7-6.3-14-14-14h-4
C15.3,0,9,6.3,9,14v6.4C10,20.1,11,20,12,20z"/>
</svg>
<h6 class="card-title">
<span id="site-ssl-status">
<div class="spinner-border spinner-border-sm" role="status"></div>{{ _('Checking SSL status..') }}</span>
</h6>
</div></a>
</div>
</div>
<div class="col-md-2">
<a style="text-decoration: none;" href="/cronjobs" target="_blank"> <div class="card" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-title="{{ _('Manage cronjobs') }}">
<div class="card-body ikona text-center">
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="cronovi" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 54 58" style="width:50px; padding-bottom:1em; enable-background:new 0 0 54 58;" xml:space="preserve">
<style type="text/css">
.st0{fill:#BFCCE0;}
.st1{fill:#192030;}
</style>
<path class="st0" d="M42,58H12C5.4000001,58,0,52.5999985,0,46V21h54v25C54,52.5999985,48.5999985,58,42,58z"/>
<path class="st1" d="M54,21H0v-3C0,11.3999996,5.4000001,6,12,6h30c6.5999985,0,12,5.3999996,12,12V21z"/>
<path class="st0" d="M13.0000019,12h-0.0000038C11.8999987,12,11,11.1000013,11,10.0000019V1.999998
C11,0.8999991,11.8999987,0,12.9999981,0h0.0000038C14.1000013,0,15,0.8999991,15,1.999998v8.0000038
C15,11.1000013,14.1000013,12,13.0000019,12z"/>
<path class="st0" d="M41.0000038,12h-0.0000076C39.8999977,12,39,11.1000013,39,10.0000019V1.999998
C39,0.8999991,39.8999977,0,40.9999962,0h0.0000076C42.1000023,0,43,0.8999991,43,1.999998v8.0000038
C43,11.1000013,42.1000023,12,41.0000038,12z"/>
<path class="st1" d="M28.0000019,35h-2.0000038C24.8999996,35,24,34.1000023,24,33.0000038v-0.0000076
C24,31.8999996,24.8999996,31,25.9999981,31h2.0000038C29.1000004,31,30,31.8999996,30,32.9999962v0.0000076
C30,34.1000023,29.1000004,35,28.0000019,35z"/>
<path class="st1" d="M15.0000019,35h-2.0000038C11.8999987,35,11,34.1000023,11,33.0000038v-0.0000076
C11,31.8999996,11.8999987,31,12.9999981,31h2.0000038C16.1000004,31,17,31.8999996,17,32.9999962v0.0000076
C17,34.1000023,16.1000004,35,15.0000019,35z"/>
<path class="st1" d="M41.0000038,35h-2.0000076C37.8999977,35,37,34.1000023,37,33.0000038v-0.0000076
C37,31.8999996,37.8999977,31,38.9999962,31h2.0000076C42.1000023,31,43,31.8999996,43,32.9999962v0.0000076
C43,34.1000023,42.1000023,35,41.0000038,35z"/>
<path class="st1" d="M28.0000019,47h-2.0000038C24.8999996,47,24,46.1000023,24,45.0000038v-0.0000076
C24,43.8999977,24.8999996,43,25.9999981,43h2.0000038C29.1000004,43,30,43.8999977,30,44.9999962v0.0000076
C30,46.1000023,29.1000004,47,28.0000019,47z"/>
<path class="st1" d="M15.0000019,47h-2.0000038C11.8999987,47,11,46.1000023,11,45.0000038v-0.0000076
C11,43.8999977,11.8999987,43,12.9999981,43h2.0000038C16.1000004,43,17,43.8999977,17,44.9999962v0.0000076
C17,46.1000023,16.1000004,47,15.0000019,47z"/>
<path class="st1" d="M41.0000038,47h-2.0000076C37.8999977,47,37,46.1000023,37,45.0000038v-0.0000076
C37,43.8999977,37.8999977,43,38.9999962,43h2.0000076C42.1000023,43,43,43.8999977,43,44.9999962v0.0000076
C43,46.1000023,42.1000023,47,41.0000038,47z"/>
</svg>
<h6 class="card-title">{{ _('Cronjobs') }}</h6>
</div>
</div></a>
</div>
{% if 'malware_scan' in enabled_modules %}
<div class="col-md-2">
<div class="card">
<a style="text-decoration: none;" href="/malware-scanner?path=/home/{{current_username}}/{{ current_domain }}" target="_blank" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-title="Scan website files with ClamAV">
<div class="card-body ikona text-center">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="scanner" x="0px" y="0px" viewBox="0 0 58 58" style="width:50px; padding-bottom:1em; enable-background:new 0 0 58 58;" xml:space="preserve"> <style type="text/css"> .st0{fill:#414954;} .st1{fill:#EBF3FF;} </style> <g> <path class="st0" d="M12,37v1c0,5.5,4.5,10,10,10h14c5.5,0,10-4.5,10-10v-1H12z"/> <path class="st0" d="M46,27v-7c0-5.5-4.5-10-10-10H22c-5.5,0-10,4.5-10,10v7H46z"/> </g> <g> <path class="st1" d="M2,14c-1.1,0-2-0.9-2-2C0,5.4,5.4,0,12,0c1.1,0,2,0.9,2,2s-0.9,2-2,2c-4.4,0-8,3.6-8,8C4,13.1,3.1,14,2,14z"/> </g> <g> <path class="st1" d="M12,58C5.4,58,0,52.6,0,46c0-1.1,0.9-2,2-2s2,0.9,2,2c0,4.4,3.6,8,8,8c1.1,0,2,0.9,2,2S13.1,58,12,58z"/> </g> <g> <path class="st1" d="M46,58c-1.1,0-2-0.9-2-2s0.9-2,2-2c4.4,0,8-3.6,8-8c0-1.1,0.9-2,2-2s2,0.9,2,2C58,52.6,52.6,58,46,58z"/> </g> <g> <path class="st1" d="M56,14c-1.1,0-2-0.9-2-2c0-4.4-3.6-8-8-8c-1.1,0-2-0.9-2-2s0.9-2,2-2c6.6,0,12,5.4,12,12 C58,13.1,57.1,14,56,14z"/> </g> <path class="st1" d="M56,34H2c-1.1,0-2-0.9-2-2v0c0-1.1,0.9-2,2-2h54c1.1,0,2,0.9,2,2v0C58,33.1,57.1,34,56,34z"/> </svg>
<h6 class="card-title">{{ _('Scanning') }}</h6>
</div>
</a>
</div>
</div>
{% endif %}
</div>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", function() {
var punycodeElements = document.querySelectorAll(".punycode");
punycodeElements.forEach(function(element) {
element.textContent = punycode.toUnicode(element.textContent);
});
});
document.addEventListener('DOMContentLoaded', function() {
const domainElement = document.querySelector('.card-text .punycode.nije-link');
const faviconElement = document.getElementById('favicon');
const currentDomain = '{{ current_domain }}';
// Create the URL to the favicon
const faviconUrl = `https://www.google.com/s2/favicons?domain=${currentDomain}`;
// Create an img element and set its src to the favicon URL
const img = document.createElement('img');
img.src = faviconUrl;
img.alt = 'Favicon';
img.style.width = '16px'; // Optional: set the size of the favicon
img.style.height = '16px'; // Optional: set the size of the favicon
// Append the img element to the favicon span
faviconElement.appendChild(img);
});
</script>
{% endif %}
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,710 @@
<!-- mautic.html -->
{% extends 'base.html' %}
{% block content %}
{% if domains %}
<style>
.toast-header {
background-color: #192030;
padding: 15px;
font-weight: 600;
font-size: 15px;
margin-bottom: 0;
line-height: 1.4;
color: rgb(255 255 255 / 84%)!important;
}
.no_link {
color: black;
text-decoration: none;
}
.nije-link {
text-decoration: none;
color: black;
border-bottom: 1px dashed black;
}
</style>
<!-- Flash messages -->
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
{% if "Error" in message %}
<script type="module">
const toastMessage = '{{ message }}';
const toast = toaster({
body: toastMessage,
header: `<div class="d-flex align-items-center" style="color: #495057;"><l-i class="bi bi-x-lg" class="me-2" style="color:red;"></l-i> {{ _("Mautic Installation failed.") }}</div>`,
});
</script>
{% else %}
<script type="module">
const toastMessage = '{{ message }}';
const toast = toaster({
body: toastMessage,
header: `<div class="d-flex align-items-center" style="color: #495057;"><l-i class="bi bi-check-lg" class="me-2" style="color:green;"></l-i> {{ _("Mautic successfully installed.") }}</div>`,
});
</script>
{% endif %}
{% endfor %}
{% endif %}
{% endwith %}
<!-- END Flash messages -->
<div class="row">
<div class="collapse mb-2" id="collapseExample">
<div class="card card-body">
<!-- Form for adding new containers -->
<div class="container">
<a href="#" class="nije-link" id="cancelLink" style="display: none;"><i class="bi bi-x-lg" style="right: 15px;top: 15px;position: absolute;color: black;padding: 6px 10px;border-radius: 50px;"></i></a>
<div class="row">
<div class="col-md-6 offset-md-3">
<h2 class="mb-3"><i class="bi bi-download"></i> {{ _("Install Mautic") }}</h2>
<p>{{ _("Install Mautic on an existing domain.") }}</p>
<form method="post" id="installForm" action="/mautic/install">
<div class="form-group row">
<div class="col-12">
<label for="website_name" class="form-label">{{ _("Website Name:") }}</label>
<input type="text" class="form-control" name="website_name" id="website_name" value="My Mautic" required>
</div>
</div>
<div class="form-group">
<label for="domain_id" class="form-label">{{ _("Domain:") }}</label>
<div class="input-group">
</select>
<select class="form-select" name="domain_id" id="domain_id">
{% for domain in domains %}
<option class="punycode" value="{{ domain.domain_id }}">{{ domain.domain_url }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="form-group">
<label for="admin_email">{{ _("Admin Email:") }}</label>
<input type="email" class="form-control" name="admin_email" id="admin_email" required>
</div>
<script>
$(document).ready(function() {
// Check if the URL contains the parameter "install"
const urlParams = new URLSearchParams(window.location.search);
const installParam = urlParams.get('install');
if (installParam || window.location.hash === '#install') {
// Show the Bootstrap collapsible element
$("#collapseExample").collapse('show');
}
// Add event listener to the dropdown
$("#domain_id").change(function() {
// Get the selected domain URL
var selectedDomain = $("#domain_id option:selected").text();
// Update the admin email input value
var adminEmailInput = $("#admin_email");
var currentAdminEmail = adminEmailInput.val();
var atIndex = currentAdminEmail.indexOf('@');
if (atIndex !== -1) {
// If '@' exists in the email, replace the part after it with the selected domain
adminEmailInput.val(currentAdminEmail.substring(0, atIndex + 1) + selectedDomain);
} else {
// If '@' doesn't exist in the email, add the selected domain after 'admin@'
adminEmailInput.val('admin@' + selectedDomain);
}
});
// Add event listener to the "Install Mautic" button to toggle form and jumbotron
$("#collapseExample").on('shown.bs.collapse', function () {
$("#jumbotronSection").hide();
$("#cancelLink").show();
});
// Add event listener to the "Cancel" link to toggle form and jumbotron
$("#cancelLink").click(function() {
$("#collapseExample").collapse('hide');
$("#jumbotronSection").show();
$("#cancelLink").hide();
});
});
var selectedDomain = $("#domain_id option:selected").text();
// Update the admin email input value
var adminEmailInput = $("#admin_email");
var currentAdminEmail = adminEmailInput.val();
var atIndex = currentAdminEmail.indexOf('@');
if (atIndex !== -1) {
// If '@' exists in the email, replace the part after it with the selected domain
adminEmailInput.val(currentAdminEmail.substring(0, atIndex + 1) + selectedDomain);
} else {
// If '@' doesn't exist in the email, add the selected domain after 'admin@'
adminEmailInput.val('admin@' + selectedDomain);
}
</script>
<div class="form-group row">
<div class="col-md-6">
<label for="admin_username" class="form-label">{{ _("Admin Username:") }}</label>
<input type="text" class="form-control" name="admin_username" id="admin_username" required>
</div>
<div class="col-md-6">
<label for="admin_password" class="form-label">{{ _("Admin Password:") }}</label>
<div class="input-group">
<input type="password" class="form-control" name="admin_password" id="admin_password" required>
<div class="input-group-append">
<button class="btn btn-outline-success" type="button" id="generatePassword">
{{ _("Generate") }}
</button>
<button class="btn btn-outline-secondary" type="button" id="togglePassword">
<i class="bi bi-eye"></i>
</button>
</div>
</div>
</div>
</div>
<script>
function generateRandomUsername(length) {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let result = "";
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * charset.length);
result += charset.charAt(randomIndex);
}
return result;
}
function generateRandomStrongPassword(length) {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+";
let result = "";
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * charset.length);
result += charset.charAt(randomIndex);
}
return result;
}
function generateInitiallyUsernameAndPassword() {
const generatedUsername = generateRandomUsername(8);
const generatedPassword = generateRandomStrongPassword(16);
document.getElementById("admin_username").value = generatedUsername;
document.getElementById("admin_password").value = generatedPassword;
};
generateInitiallyUsernameAndPassword();
document.getElementById("generatePassword").addEventListener("click", function() {
const generatedPassword = generateRandomStrongPassword(16);
document.getElementById("admin_password").value = generatedPassword;
const passwordField = document.getElementById("admin_password");
if (passwordField.type === "password") {
passwordField.type = "text";
}
});
document.getElementById("togglePassword").addEventListener("click", function() {
const passwordField = document.getElementById("admin_password");
if (passwordField.type === "password") {
passwordField.type = "text";
} else {
passwordField.type = "password";
}
});
</script>
<div class="form-group">
<label for="mautic_version" class="form-label">Mautic Version:</label>
<div class="form-field">
<select class="form-select" name="mautic_version" id="mautic_version">
</select>
</div>
</div>
<!-- https://github.com/mautic/mautic/releases -->
<br>
<button type="submit" class="btn btn-primary" id="installButton">{{ _("Start Installation") }}</button>
</form>
<script>
// Fetch the Mautic versions from the GitHub API
fetch('https://api.github.com/repos/mautic/mautic/tags')
.then(response => response.json())
.then(data => {
// Extract tag names from the response
const versions = data.map(tag => tag.name);
// Filter out versions if needed (for example, excluding beta or RC versions)
const filteredVersions = versions.filter(version => !version.includes('beta') && !version.includes('RC') && !version.includes('-'));
// Take the latest 10 versions
const latestVersions = filteredVersions.slice(0, 10);
// Populate the select dropdown with options
const selectElement = document.getElementById('mautic_version');
latestVersions.forEach(version => {
const option = document.createElement('option');
option.value = version;
option.textContent = `${version}`;
selectElement.appendChild(option);
});
})
.catch(error => {
console.error('Error fetching Mautic versions:', error);
// In case of an error, you might want to display a default option
const selectElement = document.getElementById('mautic_version');
const option = document.createElement('option');
option.value = ''; // Set a value for the default option if needed
option.textContent = 'Error fetching versions';
selectElement.appendChild(option);
});
// Function to compare two version strings
function compareVersions(a, b) {
const versionA = a.split('.').map(Number);
const versionB = b.split('.').map(Number);
for (let i = 0; i < Math.max(versionA.length, versionB.length); i++) {
const partA = versionA[i] || 0;
const partB = versionB[i] || 0;
if (partA < partB) return -1;
if (partA > partB) return 1;
}
return 0;
}
</script>
</div>
</div>
</div>
</div>
</div>
{% if data %}
{% if view_mode == 'table' %}
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>{{ _("Domain") }}</th>
<th>{{ _("Mautic Version") }}</th>
<th>{{ _("Admin Email") }}</th>
<th>{{ _("Created on") }}</th>
<th>{{ _("Actions") }}</th>
</tr>
</thead>
<tbody>
{% for row in data %}
<div class="modal fade" id="removeModal{{ row[6] }}" tabindex="-1" role="dialog" aria-labelledby="removeModalLabel{{ row[6] }}" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="removeModalLabel{{ row[6] }}">{{ row[0] }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body row">
<div class="col-md-6">
<h4>{{ _("Delete Mautic website") }}</h4>
<p>{{ _("This will irreversibly delete the website, permanently deleting all files and database.") }}</p>
<button type="button" class="btn btn-danger" onclick="confirmRemove('{{ row[6] }}')">{{ _("Uninstall") }}</button>
</div>
<div class="col-md-6">
<h4>{{ _("Remove from the Manager") }}</h4>
<p>{{ _("This will just remove the installation from the Manager but keep the files and database.") }}</p>
<button type="button" class="btn btn-warning" onclick="confirmDetach('{{ row[6] }}')">{{ _("Detach") }}</button>
</div>
</div>
<div class="modal-footer">
</div>
</div>
</div>
</div>
{% set domain_url = row[0] %}
<tr>
<td><a class="punycode no_link domain_link" href="http://{{ row[0] }}" target="_blank"><img src="https://www.google.com/s2/favicons?domain={{ row[0] }}" alt="{{ row[0] }} Favicon" style="width:16px; height:16px; margin-right:5px;">
{{ row[0] }} <i class="bi bi-box-arrow-up-right"></i></a></td>
<td>{{ row[3] }}</td>
<td>{{ row[2] }}</td>
<td>{{ row[4] }}</td>
<td>
<a class="btn btn-secondary mx-2" href="/website?domain={{ row[0] }}">{{ _("Manage") }}</a>
<button type="button" class="btn btn-danger" style="border: 0px;" data-bs-toggle="modal" data-bs-target="#removeModal{{ row[1] }}"><i class="bi bi-trash3"></i> {{ _("Remove") }}</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% if view_mode == 'cards' %}
<style>
/* Style for the container */
.image-container {
position: relative;
display: inline-block;
}
/* Style for the button */
.center-button {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: none; /* Hide the button by default */
z-index: 1; /* Ensure the button appears above the image */
}
/* Show the button when hovering over the image container */
.image-container:hover .center-button {
display: block;
}
.card {
border: none;
border-radius: 10px
}
.c-details span {
font-weight: 300;
font-size: 13px
}
.icon {
width: 50px;
height: 50px;
background-color: #eee;
border-radius: 15px;
display: flex;
align-items: center;
justify-content: center;
font-size: 39px
}
.badge span {
background-color: black;
width: 80px;
height: 20px;
padding-bottom: 3px;
border-radius: 5px;
display: flex;
color: white;
justify-content: center;
align-items: center
}
.text1 {
font-size: 14px;
font-weight: 600
}
.text2 {
color: #a5aec0
}
a.close_button {
right: 5px;top: 10px;position: absolute;color: white;padding: 0px 4px;background: indianred;border-radius: 50px; z-index:1;
}
a.close_button:hover {
background: red;
}
</style>
<div class="container mb-3">
<div class="row">
{% for row in data %}
<div class="modal fade" id="removeModal{{ row[6] }}" tabindex="-1" role="dialog" aria-labelledby="removeModalLabel{{ row[6] }}" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="removeModalLabel{{ row[6] }}">{{ row[0] }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body row">
<div class="col-md-6">
<h4>{{ _("Delete Mautic website") }}</h4>
<p>{{ _("This will irreversibly delete the website, permanently deleting all files and database.") }}</p>
<button type="button" class="btn btn-danger" onclick="confirmRemove('{{ row[6] }}')">{{ _("Uninstall") }}</button>
</div>
<div class="col-md-6">
<h4>{{ _("Remove from the Manager") }}</h4>
<p>{{ _("This will just remove the installation from the Manager but keep the files and database.") }}</p>
<button type="button" class="btn btn-warning" onclick="confirmDetach('{{ row[6] }}')">{{ _("Detach") }}</button>
</div>
</div>
<div class="modal-footer">
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card bg-light mb-2">
<a href="#" class="close_button" data-bs-toggle="modal" data-bs-target="#removeModal{{ row[6] }}"><i class="bi bi-x-lg" style=""></i></a>
<div class="mt-0">
<div class="image-container">
<a href="/website?domain={{ row[0] }}">
<img
id="screenshot-image-{{ row[0] }}"
src="/static/images/placeholder.svg"
alt="Screenshot of {{ row[0] }}"
class="img-fluid"
/>
</a>
<a href="/website?domain={{ row[0] }}" class="center-button btn btn-dark">
<i class="bi bi-sliders2-vertical"></i> {{ _("Manage") }}
</a>
</div>
</div>
<div class="d-flex p-2 justify-content-between">
<div class="d-flex flex-row align-items-center">
<div class="icon"> <i class=""><svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="42px" height="42px" viewBox="0 0 349.779 349.779" enable-background="new 0 0 349.779 349.779" xml:space="preserve"><path fill="#FFFFFF" d="M174.89,349.779C78.612,349.779,0,271.462,0,174.89S78.612,0,174.89,0c23.26,0,45.931,4.417,67.129,13.543
c5.889,2.65,8.833,9.422,6.478,15.605c-2.649,5.888-9.421,8.833-15.604,6.477c-18.549-7.655-37.98-11.482-58.002-11.482
c-83.323,0-151.041,67.718-151.041,151.041S91.567,326.225,174.89,326.225c83.323,0,151.041-67.718,151.041-151.041
c0-17.96-2.944-35.332-9.127-51.819c-2.355-6.183,0.883-12.955,7.066-15.31c6.183-2.355,12.954,0.883,15.31,7.066
c7.066,19.138,10.6,39.453,10.6,60.063C349.779,271.167,271.462,349.779,174.89,349.779"></path><g><polygon fill="#FDB933" points="251.44,156.93 224.354,185.194 239.369,248.496 273.522,248.496 "></polygon></g><polygon fill="#FDB933" points="240.253,73.312 249.674,82.734 174.89,161.935 110.999,96.277 74.196,248.496 108.35,248.496
128.665,163.996 174.89,214.343 273.817,106.583 283.239,116.299 292.66,63.007 "></polygon></svg></i> </div>
<div class="ms-2 c-details">
<h6 class="punycode mb-0"><a href="http://{{ row[0] }}" target="_blank" class="nije-link">{{ row[0] }}</a></h6> <span>{{ _("Mautic") }} {{ row[3] }}</span>
</div>
</div>
<div class="badge" style="display: -webkit-box;">
<!--div class="dropdown" style="vertical-align: middle; display: inline;">
<a href="" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-three-dots-vertical" style="font-size: 1.3125rem;"></i>
</a>
<ul class="dropdown-menu" aria-labelledby="more-actions">
<li><a class="dropdown-item" href="#">PHP Version</a></li>
<li><a class="dropdown-item" href="#">SSL Security</a></li>
<li><a class="dropdown-item" href="#">SSL Security</a></li>
<hr>
<li><a class="dropdown-item" href="#">Refresh Preview</a></li>
</ul>
</div--></div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<script>
// Function to update a screenshot image
function updateScreenshot(screenshotImage, screenshotURL) {
var imageLoader = new Image();
imageLoader.src = screenshotURL;
imageLoader.onload = function() {
screenshotImage.src = screenshotURL;
};
}
// Function to update all screenshot images
function updateAllScreenshots() {
{% for row in data %}
var screenshotImage = document.getElementById("screenshot-image-{{ row[0] }}");
var screenshotURL = "/screenshot/{{ row[0] }}";
updateScreenshot(screenshotImage, screenshotURL);
{% endfor %}
}
updateAllScreenshots();
</script>
{% endif %}
<script>
// Function to confirm removal
function confirmRemove(id) {
// Send an AJAX request to the server to remove Mautic
var xhr = new XMLHttpRequest();
xhr.open('POST', '/mautic/remove', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
// Reload the page to update the table
location.reload();
} else {
alert('{{ _("An error occurred while removing Mautic.") }}');
}
}
};
xhr.send('id=' + id);
}
// Function to confirm detachment
function confirmDetach(id) {
// Send an AJAX request to the server to remove Mautic
var xhr = new XMLHttpRequest();
xhr.open('POST', '/mautic/detach', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
// Redirect to wp manager page
window.location.href = '/mautic';
} else {
alert('{{ _("An error occurred while detaching Mautic.") }}');
}
}
};
xhr.send('id=' + id);
}
</script>
</div>
<style>
.nije-link {
text-decoration: none;
color: black;
border-bottom: 1px dashed black;
}
</style>
{% else %}
<!-- Display jumbotron for no wp installations -->
<div class="jumbotron text-center mt-5" id="jumbotronSection" style="min-height: 70vh;display:block;">
<h1>{{ _("No Mautic Installations") }}</h1>
<p>{{ _("There are no existing Mautic installations. You can install a new instance below.") }}</p>
<button class="btn btn-lg btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#collapseExample" aria-expanded="false" aria-controls="collapseExample">
<i class="bi bi-download"></i> {{ _("Install Mautic") }}
</button>
</div>
{% endif %}
{% else %}
<!-- Display jumbotron for no domains -->
<div class="jumbotron text-center mt-5" id="jumbotronSection">
<h1>{{ _("No Domains") }}</h1>
<p>{{ _("Add a domain name first in order to install Mautic.") }}</p>
<a href="/domains#add-new" class="btn btn-lg btn-primary">
<i class="bi bi-plus-lg"></i> {{ _("Add a Domain Name") }}
</a>
</div>
{% endif %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/punycode/2.1.1/punycode.min.js"></script>
<script type="module">
punnyCodeShouldBeGlobalFunction function() {
var punycodeElements = document.querySelectorAll(".punycode");
punycodeElements.forEach(function(element) {
element.textContent = punycode.toUnicode(element.textContent);
});
};
punnyCodeShouldBeGlobalFunction();
</script>
</section>
<footer class="main-footer btn-toolbar" role="toolbar">
<div class="btn-group" role="group" aria-label="Status">
</div>
<div class="ms-auto" role="group" aria-label="Actions">
<button class="btn btn-transparent" type="button" aria-expanded="false" id="refreshData">
<i class="bi bi-arrow-counterclockwise"></i> {{ _('Refresh') }}<span class="desktop-only"> {{ _('Data') }}</span>
</button>
<button class="btn btn-primary mx-2" type="button" data-bs-toggle="collapse" data-bs-target="#collapseExample" aria-expanded="false" aria-controls="collapseExample">
<i class="bi bi-download"></i> {{ _('Install') }}<span class="desktop-only"> {{ _('Mautic') }}</span>
</button>
<span class="desktop-only">{% if view_mode == 'cards' %}
<a href="{{ url_for('list_mautic', view='table') }}" data-bs-toggle="tooltip" data-bs-placement="top" title="{{ _('Switch to List View - perfect for those with many Websites.') }}" class="btn btn-outline" style="padding-left: 5px; padding-right: 5px;"><i class="bi bi-table"></i></a>
{% endif %}
{% if view_mode == 'table' %}
<a href="{{ url_for('list_mautic', view='cards') }}" data-bs-toggle="tooltip" data-bs-placement="top" title="{{ _('Switch to Grid View - a visual representation of your Websites and their content.') }}" class="btn btn-outline" style="padding-left: 5px; padding-right: 5px;"><i class="bi bi-view-list"></i></a>
{% endif %}</span>
<script>
// Add an event listener to the links to hide tooltip as its buggy!
document.addEventListener('DOMContentLoaded', function () {
var links = document.querySelectorAll('.desktop-only a');
links.forEach(function (link) {
link.addEventListener('click', function () {
// Hide the tooltip when the link is clicked
var tooltip = new bootstrap.Tooltip(link);
tooltip.hide();
});
});
});
</script>
</div>
</footer>
{% endblock %}

View File

@@ -0,0 +1,320 @@
<!-- memcached.html -->
{% extends 'base.html' %}
{% block content %}
<script type="module">
// Function to attach event listeners
function attachEventListeners() {
document.querySelectorAll("button[type='submit']").forEach((btn) => {
if (!btn.classList.contains("limits")) {
btn.addEventListener("click", async (ev) => {
ev.preventDefault();
const action = btn.closest("form").querySelector("input[name='action']").value;
let btnClass, toastMessage;
if (action === 'enable') {
btnClass = 'success';
toastMessage = "{{ _('Enabling Memcached service..') }}";
} else if (action === 'install_memcached') {
btnClass = 'primary';
toastMessage = '{{ _("Installing Memcached service.. Please wait") }}';
} else if (action === 'disable') {
btnClass = 'danger';
toastMessage = "{{ _('Disabling Memcached service..') }}";
}
const toast = toaster({
body: toastMessage,
className: `border-0 text-white bg-${btnClass}`,
});
try {
const response = await fetch(window.location.href, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `action=${action}`,
});
// get the response HTML content
const resultHtml = await response.text();
// Parse the HTML string to extract the content of the specific element
const parser = new DOMParser();
const doc = parser.parseFromString(resultHtml, 'text/html');
const mainScopeContent = doc.getElementById("main-scope")?.innerHTML;
// Replace the content of the element with the ID "main-scope"
const mainScopeElement = document.getElementById("main-scope");
if (mainScopeElement) {
mainScopeElement.innerHTML = mainScopeContent || '';
}
// Reattach event listeners after updating content
attachEventListeners();
// Reinitialize the log script
initializeLogScript();
} catch (error) {
console.error('Error:', error);
}
});
}
});
}
// Function to initialize the log script
function initializeLogScript() {
$(document).ready(function() {
$("#service-log").click(function(event) {
event.preventDefault();
$.ajax({
url: "/view-log/var/log/memcached.log",
type: "GET",
success: function(data) {
$("#log-content").html(data);
$("#log-container").show(); // Show the container when data is fetched
},
error: function() {
$("#log-content").html("{{ _("Error fetching log content.") }}");
$("#log-container").show(); // Show the container even on error
}
});
});
});
}
// Attach event listeners initially
attachEventListeners();
</script>
<div class="row g-3">
{% if memcached_status_display == 'NOT INSTALLED' %}
<div class="jumbotron text-center mt-5" id="jumbotronSection">
<h1><i class="bi bi-x-lg" style="color:red;"></i> {{_('Memcached is not currently installed.')}}</h1>
<p>{{_('To install Memcached click on the button bellow.')}}</p>
<form method="post">
<input type="hidden" name="action" value="install_memcached">
<button class="btn btn-lg btn-primary" type="submit">{{_('INSTALL MEMCACHED')}}</button>
</form>
</div>
{% elif memcached_status_display == 'ON' %}
<script>
function updateSliderValue(value) {
const allowedValues = [128, 256, 512, 1024, 2048];
const closestValue = allowedValues.reduce((prev, curr) =>
Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev
);
document.getElementById('set_memory').value = closestValue;
document.getElementById('slider_value').textContent = closestValue + ' MB';
}
</script>
<div class="col-md-4 col-xl-4">
<div class="card card-one">
<div class="card-header">
<h6 class="card-title">{{ _('Connection Info') }}</h6>
</div>
<div class="card-body">
<div class="row mt-2 mb-2">
<label class="card-title fw-medium text-dark mb-1">{{ _('status:') }}</label><div class="col-6">
<h3 class="card-value mb-1"><i class="bi bi-check-circle-fill"></i> {{ _('Active') }}</h3>
</div><!-- col -->
</div>
<hr>
<div class="row mt-2 mb-2">
<div class="col-6">
<label class="card-title fw-medium text-dark mb-1">{{ _('Memcached server:') }}</label><h3 class="card-value mb-1">127.0.0.1</h3>
<span class="d-block text-muted fs-11 ff-secondary lh-4">{{ _('*or localhost') }}</span>
</div><!-- col -->
</div>
<hr>
<div class="row mt-2 mb-2">
<div class="col-12">
<label class="card-title fw-medium text-dark mb-1">{{ _("Port:") }}</label>
<h3 class="card-value mb-1">11211</h3><span class="d-block text-muted fs-11 ff-secondary lh-4">{{ _('*Access to the service is NOT available from other servers.') }}</span>
</div><!-- col -->
</div><!-- row -->
</div><!-- card-body -->
<div class="card-footer d-flex justify-content-center">
<a href="#" class="fs-sm" id="service-log">{{ _('View Memcached service Log') }}</a>
</div>
</div><!-- card-one -->
</div>
<div class="col-md-6 col-xl-8">
<div class="card card-one">
<div class="card-header">
<h6 class="card-title">{{ _('Memcached Memory Allocation') }}</h6>
<nav class="nav nav-icon nav-icon-sm ms-auto">
<a href="" class="nav-link"><i class="ri-refresh-line"></i></a>
<a href="" class="nav-link"><i class="ri-more-2-fill"></i></a>
</nav>
</div><!-- card-header -->
<div class="card-body">
<p class="mb-3 fs-xs">{{ _('You can allocate RAM to Memcached service.') }}</p>
<form method="post">
<div class="card p-3 d-flex flex-row mb-2">
<div class="card-icon"><img src="{{ url_for('static', filename='images/icons/memcached.png') }}" style="width: 50px;"></div>
<div class="ms-3" style="width: 100%;">
<p class="fs-xs text-secondary mb-0 lh-4">{{ _('Current Memory limit for Memcached service') }}</p>
<h4 class="card-value mb-1">
{% if maxmemory_value == 0 %}
<i class="bi bi-infinity"></i>
{% else %}
{{ maxmemory_value | int }} MB
{% endif %}
</h4>
<label class="card-title fw-medium text-dark mt-5 mb-1">{{ _('Change Memory Allocation') }}</label>
<div class="form-group">
<div class="form-group">
<input type="range" style="width: 80%;" class="form-control-range" id="set_memory" name="set_memory"
min="128" max="2048" step="128" value="{{ maxmemory_value | int }}" oninput="updateSliderValue(this.value)">
<span id="slider_value">{{ maxmemory_value | int }} MB</span><br>
<button type="submit" class="limits btn text-right btn-primary mt-3">{{ _('Save') }}</button>
</div>
</div>
</form> </div>
</div>
</div><!-- col -->
</div><!-- row -->
</div>
<div class="row g-3">
<div class="col-md-6 col-xl-12" style="display: none;" id="log-container">
<div class="card card-one">
<div class="card-header">
<h6 class="card-title">{{ _('Memcached service logs') }}</h6>
<nav class="nav nav-icon nav-icon-sm ms-auto"></nav>
</div><!-- card-header -->
<div class="card-body">
<pre id="log-content"></pre>
</div><!-- card-body -->
</div><!-- card -->
</div>
</div>
{% endif %}
{% if memcached_status_display == 'ON' %}
{% elif memcached_status_display == 'NOT INSTALLED' %}
{% elif memcached_status_display == 'OFF' %}
<div class="jumbotron text-center mt-5" id="jumbotronSection">
<h1><i class="bi bi-x-lg" style="color:red;"></i> {{ _('Memcached is currently disabled.') }}</h1>
<p>{{ _('To enable Memcached SERVER click on the button bellow.') }}</p>
<form method="post">
<input type="hidden" name="action" value="enable">
<button class="btn btn-lg btn-primary" type="submit">{{ _('START Memcached') }}</button>
</form>
</div>
{% else %}
<div class="jumbotron text-center mt-5" id="jumbotronSection">
<h1><i class="bi bi-x-lg"></i> {{ _('Memcached service status is unknown.') }}</h1>
<p>{{ _('Unable to determinate current Memcached service status, try Start&Stop actions.') }} <br>{{ _('If the issue persists please contact support.') }}</p>
<form method="post">
<input type="hidden" name="action" value="enable">
<button class="btn btn-primary d-flex align-items-center gap-2" type="submit">{{ _('START') }}</button>
</form>
<form method="post">
<input type="hidden" name="action" value="disable">
<button class="btn btn-primary d-flex align-items-center gap-2" type="submit">{{ _('STOP') }}</button>
</form>
</div>
{% endif %}
</div>
<script>
$(document).ready(function() {
$("#service-log").click(function(event) {
event.preventDefault();
$.ajax({
url: "/view-log/var/log/memcached.log",
type: "GET",
success: function(data) {
$("#log-content").html(data);
$("#log-container").show(); // Show the container when data is fetched
},
error: function() {
$("#log-content").html("Error fetching log content.");
$("#log-container").show(); // Show the container even on error
}
});
});
});
</script>
</section>
<footer class="main-footer btn-toolbar" role="toolbar">
<div class="btn-group" role="group" aria-label="Status">
<label>status:</label><b> {% if memcached_status_display == 'ON' %} Enabled{% elif memcached_status_display == 'OFF' %} Disabled{% elif memcached_status_display == 'NOT INSTALLED' %} Not Installed{% else %} Unknown{% endif %}</b>
</div>
<div class="ms-auto" role="group" aria-label="Actions">
{% if memcached_status_display == 'ON' %}
<form method="post">
<input type="hidden" name="action" value="disable">
<button class="btn btn-danger d-flex align-items-center gap-2" type="submit">{{ _('Disable Memcached service') }}</button>
</form>
{% elif memcached_status_display == 'OFF' %}
<form method="post">
<input type="hidden" name="action" value="enable">
<button class="btn btn-success d-flex align-items-center gap-2" type="submit">{{ _('Enable Memcached service') }}</button>
</form>
{% elif memcached_status_display == 'NOT INSTALLED' %}
<form method="post">
<input type="hidden" name="action" value="install_memcached">
<button class="btn btn-success d-flex align-items-center gap-2" type="submit">{{_('Install Memcached')}}</button>
</form>
{% else %}
<form method="post">
<input type="hidden" name="action" value="disable">
<button class="btn btn-primary d-flex align-items-center gap-2" type="submit">{{ _('Disable Memcached service') }}</button>
</form>
<form method="post">
<input type="hidden" name="action" value="enable">
<button class="btn btn-primary d-flex align-items-center gap-2" type="submit">{{ _('Enable Memcached service') }}</button>
</form>
{% endif %}
</div>
</footer>
{% endblock %}

View File

@@ -0,0 +1,259 @@
<!-- Keyboard Shortcuts modal -->
<div
class="modal fade"
id="shortcutsModal"
tabindex="-1"
role="dialog"
aria-labelledby="shortcutsModalLabel"
aria-hidden="true"
>
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5
class="modal-title"
id="shortcutsModalLabel"
>{{ _('Available Keyboard Shortcuts') }}</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-12">
<div class="container">
{% if current_route.startswith('/files') %}
<p>{{ _('File Manager Shortcuts') }}</p>
<table class="table table-bordered">
<thead>
<tr>
<th>{{ _('Shortcut') }}</th>
<th>{{ _('Action') }}</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<strong>{{ _('Shift + A') }}</strong>
</td>
<td>{{ _('Select All') }}</td>
</tr>
<tr>
<td>
<strong>{{ _('Shift + N') }}</strong>
</td>
<td>{{ _('Create a New Folder') }}</td>
</tr>
<tr>
<td>
<strong>{{ _('Shift + U') }}</strong>
</td>
<td>{{ _('Upload Files') }}</td>
</tr>
<tr>
<td>
<strong>{{ _('Shift + C') }}</strong>
</td>
<td>{{ _('Copy Files') }}</td>
</tr>
<tr>
<td>
<strong>{{ _('Shift + M') }}</strong>
</td>
<td>{{ _('Move Files') }}</td>
</tr>
<tr>
<td>
<strong>{{ _('DELETE') }}</strong>
</td>
<td>{{ _('Delete File') }}</td>
</tr>
<tr>
<td>
<strong>{{ _('Shift + V') }}</strong>
</td>
<td>{{ _('View File') }}</td>
</tr>
<tr>
<td>
<strong>{{ _('Shift + E') }}</strong>
</td>
<td>{{ _('Edit File') }}</td>
</tr>
<tr>
<td>
<strong>{{ _('Shift + R') }}</strong>
</td>
<td>{{ _('Rename Files') }}</td>
</tr>
<tr>
<td>
<strong>{{ _('Shift + Z') }}</strong>
</td>
<td>{{ _('Compress Files') }}</td>
</tr>
<tr>
<td>
<strong>{{ _('Shift + X') }}</strong>
</td>
<td>{{ _('Extract Files') }}</td>
</tr>
<tr>
<td>
<strong>{{ _('Shift + F') }}</strong>
</td>
<td>{{ _('Home') }}</td>
</tr>
</tbody>
</table>
{% elif current_route.startswith('/usage') %}
<p>{{ _('Resource Usage Shortcuts') }}</p>
<table class="table table-bordered">
<thead>
<tr>
<th>{{ _('Shortcut') }}</th>
<th>{{ _('Action') }}</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<strong>{{ _('Shift + C') }}</strong>
</td>
<td>{{ _('Current Resource Usage') }}</td>
</tr>
<tr>
<td>
<strong>{{ _('Shift + O') }}</strong>
</td>
<td>{{ _('Historical (older) Usage') }}</td>
</tr>
<tr>
<td>
<strong>{{ _('Shift + L') }}</strong>
</td>
<td>{{ _('View logs') }}</td>
</tr>
</tbody>
</table>
{% endif %}
</div>
</div>
<div class="col-md-12">
<div class="container">
<p>{{ _('Global Shortcuts') }}</p>
<table class="table table-bordered">
<thead>
<tr>
<th>{{ _('Shortcut') }}</th>
<th>{{ _('Page') }}</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<strong>{{ _('/') }}</strong>
</td>
<td>
<i class="bi bi-search"></i>
{{ _('Search') }}
</td>
</tr>
<tr>
<td>
<strong>{{ _('Shift + H') }}</strong>
</td>
<td>
<i class="bi bi-speedometer2"></i>
{{ _('Dashboard') }}
</td>
</tr>
<tr>
<td>
<strong>{{ _('Shift + W') }}</strong>
</td>
<td>
<i class="bi bi-wordpress"></i>
{{ _('WordPress') }}
</td>
</tr>
<tr>
<td>
<strong>{{ _('Shift + F') }}</strong>
</td>
<td>
<i class="bi bi-folder"></i>
{{ _('File Manager') }}
</td>
</tr>
<tr>
<td>
<strong>{{ _('Shift + B') }}</strong>
</td>
<td>
<i class="bi bi-database"></i>
{{ _('Databases') }}
</td>
</tr>
<tr>
<td>
<strong>{{ _('Shift + P') }}</strong>
</td>
<td>
<i class="bi bi-database-exclamation"></i>
{{ _('phpMyAdmin') }}
</td>
</tr>
<tr>
<td>
<strong>{{ _('Shift + D') }}</strong>
</td>
<td>
<i class="bi bi-globe2"></i>
{{ _('Domains') }}
</td>
</tr>
<tr>
<td>
<strong>{{ _('Shift + Q') }}</strong>
</td>
<td>
<i class="bi bi-activity"></i>
{{ _('Activity') }}
</td>
</tr>
<tr>
<td>
<strong>{{ _('Shift + Y') }}</strong>
</td>
<td>
<i class="bi bi-speedometer"></i>
{{ _('Usage') }}
</td>
</tr>
<tr>
<td>
<strong>{{ _('Shift + S') }}</strong>
</td>
<td>
<i class="bi bi-gear"></i>
{{ _('Settings') }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="/static/js/keyboardShortcuts.js?v=1.0.0"></script>
</div>

View File

@@ -0,0 +1,42 @@
<!-- START Flash messages as toasts -->
{% with messages = get_flashed_messages() %}
{% if messages %}
<div aria-live="polite" aria-atomic="true" class="d-flex justify-content-center align-items-center w-100">
<div class="toast-container position-fixed top-0 p-3">
{% for message in messages %}
{% if 'success' in message.lower() %}
{% set toast_class = 'bg-success text-white' %}
{% set toast_icon = 'success' %}
{% set title = 'success' %}
{% elif 'error' in message.lower() %}
{% set toast_class = 'bg-danger text-white' %}
{% set toast_icon = 'error' %}
{% set title = 'Error' %}
{% elif 'warning' in message.lower() %}
{% set toast_class = 'bg-warning text-white' %}
{% set toast_icon = 'warning' %}
{% set title = 'Warning' %}
{% else %}
{% set toast_class = 'bg-primary text-white' %}
{% set toast_icon = 'info' %}
{% set title = 'Notification' %}
{% endif %}
<div class="toast fade show" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header {{ toast_class }}">
<strong class="me-auto">
{{ title }}
</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
<div class="d-none modal-icon modal-{{toast_icon}} modal-icon-show"></div>
{{ message }}
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% endwith %}
<!-- END Flash messages -->

View File

@@ -0,0 +1,200 @@
<div class="col-md-3">
<div class="card">
<lazy-loader>
<div class="card-body text-center" id="loading_pagespeed_data">
<!-- Loading message -->
Loading data...
</div>
</lazy-loader>
<div class="card-body text-center d-none" id="actual_data">
<style>
/*
* Widget basic styling
*/
.wrapper_ps{
width: 148px;
height: 148px;
font-family: 'Menlo','dejavu sans mono','Consolas','Lucida Console',monospace;
position: relative;
display: inline-block;
}
#desktop-meter, #mobile-meter{
width: 100%;
height: 100%;
-webkit-transform: rotate(270deg);
-moz-transform: rotate(270deg);
-o-transform: rotate(270deg);
-ms-transform: rotate(270deg);
transform: rotate(270deg);
}
.perf_percentage {
position: absolute;
width: inherit;
top: 70px;
text-align: center;
font-size: 35px;
font-weight: 600;
line-height: 1.4em;
}
/*
* Mobile display
*/
@media only screen and (max-width: 600px) {
.itps-settings-page .inner-sidebar {
width: 100%;
}
.itps-settings-page #post-body-content {
margin-right: auto;
}
}
</style>
<div class="row">
<div style="text-align: center; padding-top:10px;">
<!-- Desktop -->
<div class="wrapper_ps desktop">
<div style="text-align: center;width: inherit;">Desktop</div>
<svg id="desktop-meter">
<circle r="53" cx="50%" cy="50%" stroke="#e8eaed" stroke-width="10" fill="none"></circle>
<circle r="53" cx="50%" cy="50%" stroke="#178239" stroke-width="10" fill="none" class="frontCircle desktop_mainColor"></circle> <!-- transform="rotate(-90,240,240)" -->
</svg>
<div class="perf_percentage desktop_mainColor" id="desktop-performance_score"></div>
</div>
<!-- Mobile -->
<div class="wrapper_ps mobile">
<div style="text-align: center;width: inherit;">Mobile</div>
<svg id="mobile-meter">
<circle r="53" cx="50%" cy="50%" stroke="#e8eaed" stroke-width="10" fill="none"></circle>
<circle r="53" cx="50%" cy="50%" stroke="#178239" stroke-width="10" fill="none" class="frontCircle mobile_mainColor"></circle> <!-- transform="rotate(-90,240,240)" -->
</svg>
<div class="perf_percentage mobile_mainColor" id="mobile-performance_score"></div>
</div>
</div>
</div>
<!-- Statistics -->
<div id="statistics" style="padding: 10px; font-family: Roboto, Helvetica, Arial, sans-serif; font-size: 14px;">
<div class="stat_row" style="border-bottom: 1px solid #ebebeb; display: flex; justify-content: space-between; padding: 8px;">
<span>First Contentful Paint</span>
<div style="padding-right: 5px; font-weight: bold;">
<span class="desktop-mainColor" id="desktop-first_contentful_paint">&nbsp;</span>&nbsp;/&nbsp;<span id="mobile-first_contentful_paint" class="mobile-mainColor">&nbsp;</span>
</div>
</div>
<div class="stat_row" style="border-bottom: 1px solid #ebebeb; display: flex; justify-content: space-between; padding: 8px;">
<span>Speed Index</span>
<div style="padding-right: 5px; font-weight: bold;">
<span class="desktop-mainColor" id="desktop-speed_index"></span>&nbsp;/&nbsp;<span id="mobile-speed_index" class="mobile-mainColor"></span>
</div>
</div>
<div class="stat_row" style="border-bottom: 1px solid #ebebeb; display: flex; justify-content: space-between; padding: 8px;">
<span>Time to Interactive</span>
<div style="padding-right: 5px; font-weight: bold;">
<span class="desktop-mainColor" id="desktop-interactive"></span>&nbsp;/&nbsp;<span id="mobile-interactive" class="mobile-mainColor"></span>
</div>
</div>
<div style="color: #0000008a; font-size: 10px; text-align: right;">
Measured at <span id="timestamp">Loading...</span>
</div>
<div style="margin-top: 10px; font-size: 12px; text-align: center;">
<a href="https://developers.google.com/speed/pagespeed/insights/?url=http://{{ current_domain }}" target="_blank" style="outline: 0; text-decoration: none;">View complete results</a> on Google PageSpeed Insights.
</div>
</div>
<script>
$(document).ready(function() {
function fetchDataAndUpdateUI() {
var currentDomain = "{{ current_domain }}";
$.ajax({
url: "/json/page_speed/" + currentDomain,
type: "GET",
success: function(data) {
// Handle case where no data is available yet
if (data.message === "No data yet, please allow a few minutes for data gathering..") {
setTimeout(fetchDataAndUpdateUI, 15000); // Retry after 15 seconds
return;
}
// Update performance metrics
$('#desktop-first_contentful_paint').text(data.desktop_speed.first_contentful_paint);
$('#desktop-speed_index').text(data.desktop_speed.speed_index);
$('#desktop-interactive').text(data.desktop_speed.interactive);
$('#mobile-first_contentful_paint').text(data.mobile_speed.first_contentful_paint);
$('#mobile-speed_index').text(data.mobile_speed.speed_index);
$('#mobile-interactive').text(data.mobile_speed.interactive);
$('#timestamp').text(data.timestamp);
// Update performance metrics
$('#desktop-performance_score').text(Math.round(data.desktop_speed.performance_score * 100));
$('#mobile-performance_score').text(Math.round(data.mobile_speed.performance_score * 100));
// Determine and update colors based on performance scores
updateColors(data.desktop_speed.performance_score, 'desktop');
updateColors(data.mobile_speed.performance_score, 'mobile');
// Determine color based on score for desktop
var desktopScore = Math.round(data.desktop_speed.performance_score * 100);
var desktopColor = getColorForScore(desktopScore);
$('.desktop-mainColor').css('color', desktopColor);
// Determine color based on score for mobile
var mobileScore = Math.round(data.mobile_speed.performance_score * 100);
var mobileColor = getColorForScore(mobileScore);
$('.mobile-mainColor').css('color', mobileColor);
// Switch from loading to actual data
$('#loading_pagespeed_data').addClass('d-none');
$('#actual_data').removeClass('d-none');
},
error: function(error) {
console.log("Error fetching data:", error);
}
});
}
// Function to update colors based on performance score
function updateColors(score, type) {
var color = getColorForScore(Math.round(score * 100));
$('.' + type + '-mainColor').css('color', color); // Set text color
$('#' + type + '-meter .frontCircle').css('stroke', color); // Set circle stroke color
}
function getColorForScore(score) {
if (score >= 90) {
return '#178239';
} else if (score >= 50 && score <= 89) {
return '#e67700';
} else {
return '#c7221f';
}
}
// Fetch data and update UI on page load
fetchDataAndUpdateUI();
});
</script>
</div>
</div>
</div>

View File

@@ -0,0 +1,89 @@
{% if 'phpmyadmin' in enabled_modules %}
<div class="col-md-2">
<div class="card">
<a style="text-decoration: none;" id="phpmyadmin-link" href="#" target="_blank" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-title="Manage phpMyAdmin access on {{ current_domain }}/phpmyadmin">
<div class="card-body ikona text-center" style="padding-bottom:0;">
<svg version="1.1" id="access" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 58 58" style="width:50px; padding-bottom:1em; enable-background:new 0 0 58 58;" xml:space="preserve">
<style type="text/css">
.st0{fill:#414954;}
.st1{fill:#EBF3FF;}
</style>
<path class="st0" d="M27,0C12.1123047,0,0,12.1123047,0,27s12.1123047,27,27,27s27-12.1123047,27-27S41.8876953,0,27,0z M49.9075928,25H41.956665c-0.2987061-7.671875-2.2993774-14.3659058-5.3508911-18.8901367 C43.9279175,9.4902344,49.180542,16.6054077,49.9075928,25z M25,4.4251709V25h-8.9483643 C16.5040894,14.0232544,20.616333,6.2732544,25,4.4251709z M25,29v20.5748291 C20.616333,47.7267456,16.5040894,39.9767456,16.0516357,29H25z M29,49.5748901V29h8.9483643 C37.4959717,39.9768677,33.3839111,47.7269287,29,49.5748901z M29,25V4.4251099 C33.3839111,6.2730713,37.4959717,14.0231323,37.9483643,25H29z M17.394165,6.1100464 C14.3427124,10.6343384,12.342041,17.3282471,12.043335,25H4.0924072C4.819458,16.6055298,10.0722046,9.4905396,17.394165,6.1100464 z M4.0924072,29h7.9509277c0.2987061,7.6719971,2.2993164,14.3659058,5.350769,18.8900757 C10.0722046,44.5097046,4.819458,37.3947754,4.0924072,29z M36.6057739,47.8901978 C39.6572876,43.3660278,41.657959,36.6720581,41.956665,29h7.9509277 C49.180542,37.3948975,43.9279175,44.5098877,36.6057739,47.8901978z"/>
<circle class="st1" cx="45" cy="45" r="13"/>
<path class="st0" d="M45,50L45,50c-0.5499992,0-1-0.4500008-1-1v-8c0-0.5499992,0.4500008-1,1-1l0,0c0.5499992,0,1,0.4500008,1,1v8 C46,49.5499992,45.5499992,50,45,50z"/>
<path class="st0" d="M50,45L50,45c0,0.5499992-0.4500008,1-1,1h-8c-0.5499992,0-1-0.4500008-1-1l0,0c0-0.5499992,0.4500008-1,1-1h8 C49.5499992,44,50,44.4500008,50,45z"/>
</svg>
<h6 class="card-title" id="manage_phpmyadmin_public_title">{{ _('/phpmyadmin') }}<br><span id="phpmyadmin_public_status" class="badge bg-dark">{{ _('Checking...') }}</span></h6>
</div>
</a>
</div>
</div>
<script>
(function() {
const currentDomainForPhpmyAdminChecks = "{{ current_domain }}";
const statusElement = document.getElementById('phpmyadmin_public_status');
const phpmyadminLink = document.getElementById('phpmyadmin-link');
// Function to check the current phpMyAdmin status
function checkPhpMyAdminStatus() {
fetch(`/phpmyadmin/manage/${currentDomainForPhpmyAdminChecks}?action=status`)
.then(response => response.json())
.then(data => {
if (data.phpmyadmin_status === 'on') {
statusElement.textContent = 'Enabled';
statusElement.classList.remove('bg-dark', 'bg-danger', 'bg-warning');
statusElement.classList.add('bg-success');
phpmyadminLink.href = `/phpmyadmin/manage/${currentDomainForPhpmyAdminChecks}?action=disable`;
phpmyadminLink.setAttribute('data-bs-title', `Disable phpMyAdmin access on ${currentDomainForPhpmyAdminChecks}/phpmyadmin`);
} else {
statusElement.textContent = 'Disabled';
statusElement.classList.remove('bg-dark', 'bg-success', 'bg-warning');
statusElement.classList.add('bg-dark');
phpmyadminLink.href = `/phpmyadmin/manage/${currentDomainForPhpmyAdminChecks}?action=enable`;
phpmyadminLink.setAttribute('data-bs-title', `Enable phpMyAdmin access on ${currentDomainForPhpmyAdminChecks}/phpmyadmin`);
}
const tooltip = new bootstrap.Tooltip(phpmyadminLink);
tooltip.enable();
})
.catch(error => {
console.error('Error checking phpMyAdmin status:', error);
statusElement.textContent = 'Error';
statusElement.classList.remove('bg-success', 'bg-danger', 'bg-dark');
statusElement.classList.add('bg-warning');
});
}
// Function to toggle phpMyAdmin access
function togglePhpMyAdmin() {
const action = phpmyadminLink.getAttribute('href').split('action=')[1];
fetch(`/phpmyadmin/manage/${currentDomainForPhpmyAdminChecks}?action=${action}`, { method: 'GET' })
.then(response => response.json())
.then(data => {
if (data.message) {
checkPhpMyAdminStatus(); // Reload the status after toggling
} else {
console.error('Unexpected response format:', data);
}
})
.catch(error => {
console.error('Error toggling phpMyAdmin:', error);
});
}
checkPhpMyAdminStatus();
phpmyadminLink.addEventListener('click', function(event) {
event.preventDefault();
const tooltip = bootstrap.Tooltip.getInstance(phpmyadminLink); // Get the tooltip instance
if (tooltip) {
tooltip.hide(); // Hide the tooltip
tooltip.dispose(); // Dispose of the tooltip instance
}
togglePhpMyAdmin(); // Call the toggle function
});
})();
</script>
{% endif %}

View File

@@ -0,0 +1,18 @@
<div class="card-body screenshot" data-link="http://{{ current_domain }}">
<a id="https_link" href="http://{{ current_domain }}" target="_blank">
<img id="screenshot-image" style="width: 700px;" src="/static/images/placeholder.svg" alt="Screenshot of {{ current_domain }}" class="img-fluid">
</a>
</div>
<script>
function updateScreenshot() {
var screenshotImage = document.getElementById("screenshot-image");
var screenshotURL = "/screenshot/{{ current_domain }}";
var imageLoader = new Image();
imageLoader.src = screenshotURL;
imageLoader.onload = function() {
screenshotImage.src = screenshotURL;
};
}
window.onload = updateScreenshot;
</script>

View File

@@ -0,0 +1,936 @@
<!-- Bootstrap Icons -->
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"
/>
<aside id="sidebar" class="sidebar" data-bs-scroll="true">
<a class="sidebar-brand" href="/">
{% if logo %}
<span class="sidebar-brand-title" style="font-size: 1.9rem;">
<img src="{{logo}}" />
</span>
{% elif brand_name %}
<span
class="sidebar-brand-title text-center"
style="font-size: 1.9rem;"
>{{brand_name}}</span>
{% else %}
<span class="sidebar-brand-icon">
<svg
version="1.0"
style="vertical-align:middle;"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 213.000000 215.000000"
preserveAspectRatio="xMidYMid meet"
>
<g
transform="translate(0.000000,215.000000) scale(0.100000,-0.100000)"
fill="currentColor"
stroke="none"
>
<path
d="M990 2071 c-39 -13 -141 -66 -248 -129 -53 -32 -176 -103 -272 -158 -206 -117 -276 -177 -306 -264 -17 -50 -19 -88 -19 -460 0 -476 0 -474 94 -568 55 -56 124 -98 604 -369 169 -95 256 -104 384 -37 104 54 532 303 608 353 76 50 126 113 147 184 8 30 12 160 12 447 0 395 -1 406 -22 461 -34 85 -98 138 -317 264 -104 59 -237 136 -295 170 -153 90 -194 107 -275 111 -38 2 -81 0 -95 -5z m205 -561 c66 -38 166 -95 223 -127 l102 -58 0 -262 c0 -262 0 -263 -22 -276 -13 -8 -52 -31 -88 -51 -36 -21 -126 -72 -200 -115 l-135 -78 -3 261 -3 261 -166 95 c-91 52 -190 109 -219 125 -30 17 -52 34 -51 39 3 9 424 256 437 255 3 0 59 -31 125 -69z"
/>
</g>
</svg>
</span>
<span
class="sidebar-brand-title text-center"
style="font-size: 1.9rem;"
>{{brand}}</span>
{% endif %}
</a>
{% if user_websites %}
<!-- optional selector -->
{% if 'wordpress' in enabled_modules or 'pm2' in enabled_modules or 'mautic' in enabled_modules or 'flarum' in enabled_modules %}
<div class="dropdown">
<button
class="btn btn-secondary dropdown-toggle"
style="width: 100%;"
type="button"
id="sidebar-selector"
data-bs-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
{{ _('Select') }}
<span class="sidebar_open">{{ _('website') }}</span>
</button>
<div
class="dropdown-menu dropdown-menu-dark"
aria-labelledby="sidebar-selector"
>
{% for site in user_websites %}
<a
class="dropdown-item"
href="/website?domain={{ site[0] }}"
>
<span
class="punycode nav-link"
style="line-break: anywhere;"
>{{ site[0] }}</span>
</a>
{% endfor %}
<!-- cheap trick to add space after -->
</div>
</div>
{% endif %}
<div class="sidebar-profile dropdown">
{% if avatar_type == 'gravatar' %}
<a href="/account"><img src="{{ gravatar_image_url }}" class="avatar avatar-thumb rounded cursor-pointer" alt="{{ current_username }}"></a>
{% elif avatar_type == 'letter' %}
<div style="display: block;float: left;margin-right: 0.5rem;" class="avatar avatar-thumb bg-primary cursor-pointer"><span style="text-align: center;font-size: 2em;">{{ current_username[0] }}</span></div>
{% else %}
<i style="display: block;float: left;margin-right: 0.5rem;font-size: xx-large;" class="bi bi-person-circle"></i>
{% endif %}
<a href="/account" class="btn btn-link sidebar-profile-name">{{ current_username }}</a>
<div class="sidebar-profile-subtitle">{{ _('Administrator') }}</div>
</div>
{% if 'wordpress' in enabled_modules and 'pm2' in enabled_modules %}
<a
href="#"
id="openModalBtn"
data-bs-toggle="modal"
data-bs-target="#cardModal"
class="new_website_or_new_domain_main btn btn-sm btn-primary"
role="button"
>
<i class="bi bi-plus-lg"></i>
{{ _('New') }}
<span class="sidebar_open">{{ _('Website') }}</span>
</a>
{% endif %}
{% else %}
{% if total_domains_count == 0 %}
<a
href="/domains#add-new"
class="new_website_or_new_domain_main btn btn-sm btn-primary"
role="button"
>
<i class="bi bi-plus-lg"></i>
{{ _('Add') }}
<span class="sidebar_open">{{ _('your first domain') }}</span>
</a>
{% else %}
{% if 'wordpress' in enabled_modules and 'pm2' in enabled_modules %}
<a
href
id="openModalBtn"
data-bs-toggle="modal"
data-bs-target="#cardModal"
class="new_website_or_new_domain_main btn btn-sm btn-primary"
role="button"
>
<i class="bi bi-plus-lg"></i>
{{ _('Set up your website') }}
</a>
{% endif %}
{% endif %}
{% endif %}
<!-- sidebar-content -->
<div class="sidebar-content scroller">
<ul class="sidebar-nav" style="font-size:larger;">
{% if 'favorites' in enabled_modules %}
<li class="sidebar-nav-header">{{ _('Favorites') }}</li>
<div id="favorites-list"></div>
<script>
// Function to fetch favorites and populate the sidebar
function loadFavorites() {
fetch('/favorites')
.then(response => response.json())
.then(favorites => {
const favoritesList = document.getElementById('favorites-list');
favoritesList.innerHTML = ''; // Clear existing favorites
var currentPagePathFull = new URL(window.location.href);
var currentPagePath = currentPagePathFull.pathname + currentPagePathFull.search + currentPagePathFull.hash;
let isFavorite = false;
favorites.forEach(favorite => {
const { link, title } = favorite;
// Check if the current page is a favorite
if (currentPagePath === link) {
isFavorite = true;
}
const listItem = document.createElement('li');
listItem.className = 'sidebar-item favorite-link';
const linkItem = document.createElement('a');
linkItem.href = link;
linkItem.className = 'sidebar-link';
linkItem.innerHTML = `
<l-i class="bi bi-star"></l-i>
<span>${title}</span>
`;
listItem.appendChild(linkItem);
favoritesList.appendChild(listItem);
});
// If the current page is a favorite, change the button icon
if (isFavorite) {
const favoriteButton = document.getElementById('addFavoriteBtn');
favoriteButton.innerHTML = `
<l-i class="bi bi-star-fill" value="light" style="color:orange;font-size: large;"></l-i>
`;
// Update the button to indicate it's already in favorites
favoriteButton.dataset.isFavorite = 'true';
} else {
const favoriteButton = document.getElementById('addFavoriteBtn');
favoriteButton.dataset.isFavorite = 'false';
}
})
.catch(error => {
console.error('Error fetching favorites for user:', error);
});
}
//document.addEventListener('DOMContentLoaded', loadFavorites);
loadFavorites();
</script>
{% endif %}
{% if 'wordpress' in enabled_modules or 'pm2' in enabled_modules or 'mautic' in enabled_modules or 'flarum' in enabled_modules %}
<li class="sidebar-nav-header">{{ _('Applications') }}</li>
{% endif %}
{% if 'wordpress' in enabled_modules or 'pm2' in enabled_modules or 'mautic' in enabled_modules or 'flarum' in enabled_modules %}
<li class="sidebar-item">
<a
href="/sites"
class="sidebar-link {% if current_route.startswith('/sites') or current_route.startswith('website') or current_route.startswith('/pm2') or current_route.startswith('/website') or current_route.startswith('/mautic') or current_route.startswith('/flarum') %}active{% endif %}"
>
<l-i class="bi bi-app-indicator"></l-i>
<span>{{ _('Site Manager') }}</span>
</a>
</li>
<li class="sidebar-item">
<a
href="/auto-installer"
class="sidebar-link {% if current_route.startswith('/auto-installer') %}active{% endif %}"
>
<l-i class="bi bi-stars"></l-i>
<span>{{ _('Auto Installer') }}</span>
</a>
</li>
{% endif %}
<li class="sidebar-nav-header">{{ _('Files') }}</li>
<li class="sidebar-item">
<a
href="/files"
class="sidebar-link {% if current_route.startswith('/files') %}active{% endif %}"
>
<l-i class="bi bi-folder"></l-i>
<span>{{ _('File Manager') }}</span>
</a>
</li>
{% if 'ftp' in enabled_modules %}
<li class="sidebar-item">
<a
href="/ftp"
class="sidebar-link {% if current_route.startswith('/ftp') %}active{% endif %}"
>
<l-i class="bi bi-folder-symlink"></l-i>
<span>{{ _('FTP Accounts') }}</span>
</a>
</li>
{% endif %}
{% if 'backups' in enabled_modules %}
<li class="sidebar-item">
<a
href="/backups"
class="sidebar-link {% if current_route.startswith('/backup') %}active{% endif %}"
>
<l-i class="bi bi-folder-check"></l-i>
<span>{{ _('Backup & Restore') }}</span>
</a>
</li>
{% endif %}
{% if 'malware_scan' in enabled_modules %}
<li class="sidebar-item">
<a
href="/malware-scanner"
class="sidebar-link {% if current_route.startswith('/malware-scanner') %}active{% endif %}"
>
<l-i class="bi bi-upc-scan"></l-i>
<span>{{ _('ClamAV Scanner') }}</span>
</a>
</li>
{% endif %}
{% if 'disk_usage' in enabled_modules %}
<li class="sidebar-item">
<a
href="/disk-usage"
class="sidebar-link {% if current_route.startswith('/disk-usage') %}active{% endif %}"
>
<l-i class="bi bi-folder-plus"></l-i>
<span>{{ _('Disk Usage') }}</span>
</a>
</li>
{% endif %}
{% if 'inodes' in enabled_modules %}
<li class="sidebar-item">
<a
href="/inodes-explorer"
class="sidebar-link {% if current_route.startswith('/inodes-explorer') %}active{% endif %}"
>
<l-i class="bi bi-folder-x"></l-i>
<span>{{ _('Inodes Explorer') }}</span>
</a>
</li>
{% endif %}
{% if 'fix_permissions' in enabled_modules %}
<li class="sidebar-item">
<a
href="/fix-permissions"
class="sidebar-link {% if current_route.startswith('/fix-permissions') %}active{% endif %}"
>
<l-i class="bi bi-file-binary-fill"></l-i>
<span>{{ _('Fix Permissions') }}</span>
</a>
</li>
{% endif %}
{% if 'emails' in enabled_modules %}
<li class="sidebar-nav-header">{{ _('Emails') }}</li>
<li class="sidebar-item">
<a
href="/emails"
class="sidebar-link {% if current_route == '/emails' %}active{% endif %}"
>
<l-i class="bi bi-envelope"></l-i>
<span>{{ _('Email Accounts') }}</span>
</a>
</li>
<li class="sidebar-item">
<a
href="/webmail/" target="_blank"
class="sidebar-link {% if current_route == '/webmail' %}active{% endif %}"
>
<l-i class="bi bi-envelope-at"></l-i>
<span>{{ _('Webmail') }}</span>
</a>
</li>
<li class="d-none sidebar-item">
<a
href="/emails/forwarders"
class="sidebar-link {% if current_route.startswith('/emails/forwarders') %}active{% endif %}"
>
<l-i class="bi bi-envelope-exclamation"></l-i>
<span>{{ _('Forwarders') }}</span>
</a>
</li>
<li class="d-none sidebar-item">
<a
href="/emails/autoresponders"
class="sidebar-link {% if current_route.startswith('/emails/autoresponders') %}active{% endif %}"
>
<l-i class="bi bi-envelope-check"></l-i>
<span>{{ _('Autoresponders') }}</span>
</a>
</li>
<li class="d-none sidebar-item">
<a
href="/emails/track-delivery"
class="sidebar-link {% if current_route.startswith('/emails/track-delivery') %}active{% endif %}"
>
<l-i class="bi bi-envelope"></l-i>
<span>{{ _('Track Delivery') }}</span>
</a>
</li>
<li class="d-none sidebar-item">
<a
href="/emails/filters"
class="sidebar-link {% if current_route.startswith('/emails/filters') %}active{% endif %}"
>
<l-i class="bi bi-envelope-slash"></l-i>
<span>{{ _('Email Filters') }}</span>
</a>
</li>
<li class="d-none sidebar-item">
<a
href="/emails/disk-usage"
class="sidebar-link {% if current_route.startswith('/emails/disk-usage') %}active{% endif %}"
>
<l-i class="bi bi-envelope-arrow-down"></l-i>
<span>{{ _('Email Disk Usage') }}</span>
</a>
</li>
{% endif %}
<li class="sidebar-nav-header">{{ _('Databases') }}</li>
<li class="sidebar-item">
<a
href="/databases"
class="sidebar-link {% if current_route == '/databases' %}active{% endif %}"
>
<l-i class="bi bi-database"></l-i>
<span>{{ _('MySQL Databases') }}</span>
</a>
</li>
{% if 'phpmyadmin' in enabled_modules %}
<li class="sidebar-item">
<a
href="/phpmyadmin?route=/database/structure&server=1"
target="_blank"
class="sidebar-link"
>
<l-i class="bi bi-database-gear"></l-i>
<span>{{ _('phpMyAdmin') }}</span>
</a>
</li>
{% endif %}
<li class="sidebar-item">
<a
href="/databases/remote-mysql"
class="sidebar-link {% if current_route.startswith('/databases/remote-mysql') %}active{% endif %}"
>
<l-i class="bi bi-database-exclamation"></l-i>
<span>{{ _('Remote MySQL') }}</span>
</a>
</li>
<li class="sidebar-item">
<a
href="/databases/processlist"
class="sidebar-link {% if current_route.startswith('/databases/processlist') %}active{% endif %}"
>
<l-i class="bi bi-database-slash"></l-i>
<span>{{ _('Show Processes') }}</span>
</a>
</li>
<li class="sidebar-nav-header">{{ _('Domains') }}</li>
<li class="sidebar-item">
<a
href="/domains"
class="sidebar-link {% if current_route == '/domains' %}active{% endif %}"
>
<l-i class="bi bi-globe2"></l-i>
<span>{{ _('Domain Names') }}</span>
</a>
</li>
<li class="sidebar-item">
<a
href="/domains/edit-dns-zone"
class="sidebar-link {% if current_route.startswith('/domains/edit-dns-zone') %}active{% endif %}"
>
<l-i class="bi bi-pencil-square"></l-i>
<span>{{ _('DNS Zone Editor') }}</span>
</a>
</li>
<li class="sidebar-item">
<a
href="/ssl"
class="sidebar-link {% if current_route.startswith('/ssl') %}active{% endif %}"
>
<l-i class="bi bi-lock"></l-i>
<span>{{ _('SSL Certificates') }}</span>
</a>
</li>
{% if 'redis' in enabled_modules or 'memcached' in enabled_modules or 'elasticsearch' in enabled_modules%}
<li class="sidebar-nav-header">{{ _('Caching & Search') }}</li>
{% endif %}
{% if 'redis' in enabled_modules %}
<li class="sidebar-item">
<a
href="/cache/redis"
class="sidebar-link {% if current_route.startswith('/cache/redis') %}active{% endif %}"
>
<l-i>
<svg
width="20px"
height="20px"
viewBox="0 -18 256 256"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMinYMin meet"
>
<path
d="M245.97 168.943c-13.662 7.121-84.434 36.22-99.501 44.075-15.067 7.856-23.437 7.78-35.34 2.09-11.902-5.69-87.216-36.112-100.783-42.597C3.566 169.271 0 166.535 0 163.951v-25.876s98.05-21.345 113.879-27.024c15.828-5.679 21.32-5.884 34.79-.95 13.472 4.936 94.018 19.468 107.331 24.344l-.006 25.51c.002 2.558-3.07 5.364-10.024 8.988"
fill="#912626"
/>
<path
d="M245.965 143.22c-13.661 7.118-84.431 36.218-99.498 44.072-15.066 7.857-23.436 7.78-35.338 2.09-11.903-5.686-87.214-36.113-100.78-42.594-13.566-6.485-13.85-10.948-.524-16.166 13.326-5.22 88.224-34.605 104.055-40.284 15.828-5.677 21.319-5.884 34.789-.948 13.471 4.934 83.819 32.935 97.13 37.81 13.316 4.881 13.827 8.9.166 16.02"
fill="#C6302B"
/>
<path
d="M245.97 127.074c-13.662 7.122-84.434 36.22-99.501 44.078-15.067 7.853-23.437 7.777-35.34 2.087-11.903-5.687-87.216-36.112-100.783-42.597C3.566 127.402 0 124.67 0 122.085V96.206s98.05-21.344 113.879-27.023c15.828-5.679 21.32-5.885 34.79-.95C162.142 73.168 242.688 87.697 256 92.574l-.006 25.513c.002 2.557-3.07 5.363-10.024 8.987"
fill="#912626"
/>
<path
d="M245.965 101.351c-13.661 7.12-84.431 36.218-99.498 44.075-15.066 7.854-23.436 7.777-35.338 2.087-11.903-5.686-87.214-36.112-100.78-42.594-13.566-6.483-13.85-10.947-.524-16.167C23.151 83.535 98.05 54.148 113.88 48.47c15.828-5.678 21.319-5.884 34.789-.949 13.471 4.934 83.819 32.933 97.13 37.81 13.316 4.88 13.827 8.9.166 16.02"
fill="#C6302B"
/>
<path
d="M245.97 83.653c-13.662 7.12-84.434 36.22-99.501 44.078-15.067 7.854-23.437 7.777-35.34 2.087-11.903-5.687-87.216-36.113-100.783-42.595C3.566 83.98 0 81.247 0 78.665v-25.88s98.05-21.343 113.879-27.021c15.828-5.68 21.32-5.884 34.79-.95C162.142 29.749 242.688 44.278 256 49.155l-.006 25.512c.002 2.555-3.07 5.361-10.024 8.986"
fill="#912626"
/>
<path
d="M245.965 57.93c-13.661 7.12-84.431 36.22-99.498 44.074-15.066 7.854-23.436 7.777-35.338 2.09C99.227 98.404 23.915 67.98 10.35 61.497-3.217 55.015-3.5 50.55 9.825 45.331 23.151 40.113 98.05 10.73 113.88 5.05c15.828-5.679 21.319-5.883 34.789-.948 13.471 4.935 83.819 32.934 97.13 37.811 13.316 4.876 13.827 8.897.166 16.017"
fill="#C6302B"
/>
<path
d="M159.283 32.757l-22.01 2.285-4.927 11.856-7.958-13.23-25.415-2.284 18.964-6.839-5.69-10.498 17.755 6.944 16.738-5.48-4.524 10.855 17.067 6.391M131.032 90.275L89.955 73.238l58.86-9.035-17.783 26.072M74.082 39.347c17.375 0 31.46 5.46 31.46 12.194 0 6.736-14.085 12.195-31.46 12.195s-31.46-5.46-31.46-12.195c0-6.734 14.085-12.194 31.46-12.194"
fill="#FFF"
/>
<path
d="M185.295 35.998l34.836 13.766-34.806 13.753-.03-27.52"
fill="#621B1C"
/>
<path
d="M146.755 51.243l38.54-15.245.03 27.519-3.779 1.478-34.791-13.752"
fill="#9A2928"
/>
</svg>
</l-i>
<span>{{ _('REDIS') }}</span>
</a>
</li>
{% endif %}
{% if 'memcached' in enabled_modules %}
<li class="sidebar-item">
<a
href="/cache/memcached"
class="sidebar-link {% if current_route.startswith('/cache/memcached') %}active{% endif %}"
>
<l-i>
<?xml version="1.0" encoding="UTF-8"?>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="20px"
height="20px"
viewBox="0 0 20 20"
version="1.1"
>
<defs>
<linearGradient
id="linear0"
gradientUnits="userSpaceOnUse"
x1="255.894"
y1="59.789"
x2="255.894"
y2="-452"
gradientTransform="matrix(0.0390787,0,0,0.0390787,0.0000003125,17.663591)"
>
<stop
offset="0"
style="stop-color:rgb(34.117647%,29.803922%,29.019608%);stop-opacity:1;"
/>
<stop
offset="1"
style="stop-color:rgb(50.196078%,44.313725%,42.745098%);stop-opacity:1;"
/>
</linearGradient>
<linearGradient
id="linear1"
gradientUnits="userSpaceOnUse"
x1="380.442"
y1="-51.758"
x2="191.971"
y2="-382.305"
gradientTransform="matrix(0.0390787,0,0,0.0390787,0.0000003125,17.663591)"
>
<stop
offset="0"
style="stop-color:rgb(14.901961%,55.294118%,51.372549%);stop-opacity:1;"
/>
<stop
offset="1"
style="stop-color:rgb(18.039216%,63.137255%,61.960784%);stop-opacity:1;"
/>
</linearGradient>
<radialGradient
id="radial0"
gradientUnits="userSpaceOnUse"
cx="62.417"
cy="142.923"
fx="62.417"
fy="142.923"
r="9.213"
gradientTransform="matrix(0.0789391,0,0,0.0789391,3.615957,3.719512)"
>
<stop
offset="0"
style="stop-color:rgb(85.882353%,48.627451%,48.627451%);stop-opacity:1;"
/>
<stop
offset="1"
style="stop-color:rgb(78.431373%,21.568627%,21.568627%);stop-opacity:1;"
/>
</radialGradient>
<radialGradient
id="radial1"
gradientUnits="userSpaceOnUse"
cx="96.726"
cy="142.923"
fx="96.726"
fy="142.923"
r="9.213"
gradientTransform="matrix(0.0789391,0,0,0.0789391,3.615957,3.719512)"
>
<stop
offset="0"
style="stop-color:rgb(85.882353%,48.627451%,48.627451%);stop-opacity:1;"
/>
<stop
offset="1"
style="stop-color:rgb(78.431373%,21.568627%,21.568627%);stop-opacity:1;"
/>
</radialGradient>
<filter
id="alpha"
filterUnits="objectBoundingBox"
x="0%"
y="0%"
width="100%"
height="100%"
>
<feColorMatrix
type="matrix"
in="SourceGraphic"
values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"
/>
</filter>
<mask id="mask0">
<g filter="url(#alpha)">
<rect
x="0"
y="0"
width="20"
height="20"
style="fill:rgb(0%,0%,0%);fill-opacity:0.101961;stroke:none;"
/>
</g>
</mask>
<clipPath id="clip1">
<rect x="0" y="0" width="20" height="20" />
</clipPath>
<g id="surface5" clip-path="url(#clip1)">
<path
style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;"
d="M 15.539062 3.976562 C 15.984375 6.980469 16.085938 9.859375 16.078125 12.003906 C 16.070312 14.199219 15.945312 15.625 15.945312 15.625 L 13.359375 15.625 L 13.082031 15.835938 L 16.152344 15.835938 C 16.152344 15.835938 16.667969 10 15.683594 3.769531 Z M 7.398438 3.902344 C 8.203125 4.800781 9.601562 7.183594 9.792969 7.183594 C 9.28125 6.527344 8.023438 4.441406 7.398438 3.902344 Z M 6.246094 6.941406 C 5.371094 6.964844 6.421875 14.070312 6.714844 15.625 L 4.03125 15.625 L 3.847656 15.835938 L 6.921875 15.835938 C 6.632812 14.289062 5.589844 7.246094 6.441406 7.148438 C 6.363281 7.03125 6.292969 6.949219 6.246094 6.941406 Z M 13.34375 6.941406 C 12.875 7.015625 10.738281 12.96875 10.738281 12.96875 C 10.738281 12.96875 10.265625 12.910156 9.792969 12.910156 C 9.511719 12.910156 9.273438 12.929688 9.109375 12.945312 L 9.058594 13.179688 C 9.058594 13.179688 9.527344 13.121094 10.003906 13.121094 C 10.476562 13.121094 10.945312 13.179688 10.945312 13.179688 C 10.945312 13.179688 13.066406 7.261719 13.542969 7.148438 C 13.492188 7.019531 13.429688 6.945312 13.34375 6.941406 Z M 13.34375 6.941406 "
/>
</g>
<mask id="mask1">
<g filter="url(#alpha)">
<rect
x="0"
y="0"
width="20"
height="20"
style="fill:rgb(0%,0%,0%);fill-opacity:0.301961;stroke:none;"
/>
</g>
</mask>
<clipPath id="clip2">
<rect x="0" y="0" width="20" height="20" />
</clipPath>
<g id="surface8" clip-path="url(#clip2)">
<path
style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;"
d="M 4.316406 3.769531 C 3.335938 10 3.847656 15.835938 3.847656 15.835938 L 4.03125 15.632812 C 3.9375 14.253906 3.691406 9.277344 4.527344 3.980469 L 7.371094 3.980469 C 7.429688 3.980469 7.511719 4.027344 7.605469 4.109375 C 7.417969 3.902344 7.265625 3.773438 7.164062 3.769531 Z M 12.839844 3.769531 C 12.300781 3.777344 10.238281 7.390625 10 7.390625 C 10.097656 7.515625 10.175781 7.601562 10.210938 7.601562 C 10.445312 7.601562 12.507812 3.984375 13.046875 3.980469 L 15.558594 3.980469 L 15.683594 3.773438 Z M 6.648438 7.359375 C 7.339844 8.394531 9.058594 13.179688 9.058594 13.179688 L 9.109375 12.945312 C 8.683594 11.78125 7.066406 7.421875 6.660156 7.359375 C 6.65625 7.359375 6.652344 7.359375 6.648438 7.359375 Z M 13.757812 7.359375 C 14.222656 8.539062 13.34375 14.4375 13.082031 15.835938 L 13.359375 15.617188 C 13.714844 13.53125 14.574219 7.378906 13.757812 7.359375 Z M 13.757812 7.359375 "
/>
</g>
</defs>
<g id="surface1">
<path
style=" stroke:none;fill-rule:nonzero;fill:url(#linear0);"
d="M 0 13.511719 L 0 6.488281 C 0 0.8125 0.808594 0 6.480469 0 L 13.519531 0 C 19.191406 0 20 0.8125 20 6.488281 L 20 13.511719 C 20 19.1875 19.191406 20 13.519531 20 L 6.480469 20 C 0.808594 20 0 19.1875 0 13.511719 Z M 0 13.511719 "
/>
<path
style=" stroke:none;fill-rule:nonzero;fill:url(#linear1);"
d="M 4.316406 3.769531 C 3.335938 10 3.847656 15.835938 3.847656 15.835938 L 6.921875 15.835938 C 6.628906 14.277344 5.582031 7.171875 6.453125 7.148438 C 6.921875 7.222656 9.058594 13.179688 9.058594 13.179688 C 9.058594 13.179688 9.527344 13.121094 10 13.121094 C 10.476562 13.121094 10.945312 13.179688 10.945312 13.179688 C 10.945312 13.179688 13.082031 7.222656 13.550781 7.148438 C 14.421875 7.171875 13.375 14.277344 13.082031 15.835938 L 16.15625 15.835938 C 16.15625 15.835938 16.667969 10 15.683594 3.769531 L 12.839844 3.769531 C 12.300781 3.777344 10.238281 7.390625 10 7.390625 C 9.765625 7.390625 7.703125 3.777344 7.164062 3.769531 Z M 4.316406 3.769531 "
/>
<path
style=" stroke:none;fill-rule:nonzero;fill:url(#radial0);"
d="M 9.394531 15.109375 C 9.394531 15.507812 9.070312 15.835938 8.667969 15.835938 C 8.265625 15.835938 7.941406 15.507812 7.941406 15.109375 C 7.941406 14.707031 8.265625 14.378906 8.667969 14.378906 C 9.070312 14.378906 9.394531 14.707031 9.394531 15.109375 Z M 9.394531 15.109375 "
/>
<path
style=" stroke:none;fill-rule:nonzero;fill:url(#radial1);"
d="M 12.0625 15.109375 C 12.0625 15.507812 11.738281 15.835938 11.335938 15.835938 C 10.933594 15.835938 10.609375 15.507812 10.609375 15.109375 C 10.609375 14.707031 10.933594 14.378906 11.335938 14.378906 C 11.738281 14.378906 12.0625 14.707031 12.0625 15.109375 Z M 12.0625 15.109375 "
/>
<use xlink:href="#surface5" mask="url(#mask0)" />
<use xlink:href="#surface8" mask="url(#mask1)" />
</g>
</svg>
</l-i>
<span>{{ _('Memcached') }}</span>
</a>
</li>
{% endif %}
{% if 'elasticsearch' in enabled_modules %}
<li class="sidebar-item">
<a
href="/search/elasticsearch"
class="sidebar-link {% if current_route.startswith('/search/elasticsearch') %}active{% endif %}"
>
<l-i>
<?xml version="1.0" encoding="UTF-8"?>
<svg
width="20px"
height="20px"
viewBox="0 0 256 286"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
preserveAspectRatio="xMidYMid"
>
<g>
<path
d="M14.3443,80.1733 L203.5503,80.1733 C224.4013,80.1733 243.0203,70.6123 255.5133,55.6863 C229.4533,21.8353 188.5523,0.0003 142.5303,0.0003 C86.1783,0.0003 37.4763,32.7113 14.3443,80.1733"
fill="#F0BF1A"
/>
<path
d="M187.5152,102.4438 L5.7552,102.4438 C2.0332,115.1648 0.0002,128.6068 0.0002,142.5298 C0.0002,156.4538 2.0332,169.8968 5.7552,182.6168 L187.5152,182.6168 C209.3402,182.6168 227.6022,164.8008 227.6022,142.5298 C227.6022,120.2598 209.7862,102.4438 187.5152,102.4438"
fill="#07A5DE"
/>
<path
d="M255.9996,228.7548 C243.5856,214.1638 225.1166,204.8868 204.4406,204.8868 L14.3446,204.8868 C37.4766,252.3498 86.1786,285.0598 142.5296,285.0598 C188.8356,285.0598 229.9656,262.9628 255.9996,228.7548"
fill="#3EBEB0"
/>
<path
d="M5.7555,102.4438 C2.0325,115.1648 0.0005,128.6068 0.0005,142.5298 C0.0005,156.4538 2.0325,169.8968 5.7555,182.6168 L124.7135,182.6168 C127.8315,170.5908 129.6125,157.2288 129.6125,142.5298 C129.6125,127.8318 127.8315,114.4698 124.7135,102.4438 L5.7555,102.4438 Z"
fill="#231F20"
/>
<path
d="M70.8199,19.1528 C46.7669,33.4058 26.7239,54.7848 14.2529,80.1738 L119.3689,80.1738 C108.6789,55.6758 91.7539,35.1878 70.8199,19.1528"
fill="#D7A229"
/>
<path
d="M75.274,268.1347 C95.762,251.6547 112.242,229.8297 122.487,204.8867 L14.253,204.8867 C27.615,231.6117 48.995,253.8817 75.274,268.1347"
fill="#019B8F"
/>
</g>
</svg>
</l-i>
<span>{{ _('ElasticSearch') }}</span>
</a>
</li>
{% endif %}
{% if 'usage' in enabled_modules or 'domain_visitors' in enabled_modules or 'login_history' in enabled_modules %}
<li class="sidebar-nav-header">{{ _('Analytics') }}</li>
{% endif %}
{% if 'usage' in enabled_modules %}
<li class="sidebar-item">
<a
href="/usage"
class="sidebar-link {% if current_route.startswith ('/usage') %}active{% endif %}"
>
<l-i class="bi bi-speedometer2"></l-i>
<span>{{ _('Resource Usage') }}</span>
</a>
</li>
{% endif %}
{% if 'domain_visitors' in enabled_modules %}
<li class="sidebar-item">
<a
href="/domains/log"
class="sidebar-link {% if current_route.startswith('/domains/log') %}active{% endif %}"
>
<l-i class="bi bi-graph-up"></l-i>
<span>{{ _('Domain Visitors') }}</span>
</a>
</li>
{% endif %}
{% if 'login_history' in enabled_modules %}
<li class="sidebar-item">
<a
href="/activity"
class="sidebar-link {% if current_route.startswith('/activity') %}active{% endif %}"
>
<l-i class="bi bi-globe-americas"></l-i>
<span>{{ _('Account Activity') }}</span>
</a>
</li>
{% endif %}
{% if 'crons' in enabled_modules or 'ssh' in enabled_modules or 'terminal' in enabled_modules or 'usage' in enabled_modules or 'process_manager' in enabled_modules or 'webserver' in enabled_modules%}
<li class="sidebar-nav-header">{{ _('Advanced') }}</li>
{% endif %}
{% if 'crons' in enabled_modules %}
<li class="sidebar-item">
<a
href="/cronjobs"
class="sidebar-link {% if current_route == '/cronjobs' %}active{% endif %}"
>
<l-i class="bi bi-clock-history"></l-i>
<span>{{ _('Cron Jobs') }}</span>
</a>
</li>
{% endif %}
{% if 'ssh' in enabled_modules %}
<li class="sidebar-item">
<a
href="/ssh"
class="sidebar-link {% if current_route == '/ssh' %}active{% endif %}"
>
<l-i class="bi bi-terminal"></l-i>
<span>{{ _('SSH Access') }}</span>
</a>
</li>
{% endif %}
{% if 'terminal' in enabled_modules %}
<li class="sidebar-item">
<a
href="/terminal"
class="sidebar-link {% if current_route.startswith('/terminal')%}active{% endif %}"
>
<l-i class="bi bi-terminal-x"></l-i>
<span>{{ _('Web Terminal') }}</span>
</a>
</li>
{% endif %}
{% if 'process_manager' in enabled_modules %}
<li class="sidebar-item">
<a
href="/process-manager"
class="sidebar-link has-sub {% if current_route.startswith('/process-manager')%}active{% endif %}"
>
<l-i class="bi bi-cpu"></l-i>
<span>{{ _('Process Manager') }}</span>
</a>
</li>
{% endif %}
{% if 'services' in enabled_modules %}
<li class="sidebar-item">
<a
href="/server/settings"
class="sidebar-link has-sub {% if current_route.startswith('/server') or current_route.startswith('/server/mysql_conf')%}active{% endif %}"
>
<l-i class="bi bi-hdd-network"></l-i>
<span>{{ _('Server Settings') }}</span>
</a>
</li>
{% endif %}
</ul>
</div>
<!-- /sidebar-content -->
<!-- sidebar-footer -->
<div class="sidebar-footer">
<button class="btn btn-default sidebar-toggle js-sidebar-toggle">
<l-i class="bi bi-chevron-left"></l-i>
</button>
<div class="dropup">
<button
class="btn btn-default"
id="help-dropdown-btn"
data-bs-toggle="dropdown"
>
<l-i class="bi bi-info-circle-fill"></l-i>
</button>
<ul
class="dropdown-menu dropdown-menu-dark"
aria-labelledby="help-dropdown-btn"
>
<li>
<a
class="dropdown-item"
target="_blank"
href="https://openpanel.com/docs/panel/intro"
>{{ _('Documentation') }}</a>
</li>
<li>
<a
class="dropdown-item"
target="_blank"
href="https://community.openpanel.com/t/openpanel"
>{{ _('Support Forums') }}</a>
</li>
<li>
<hr class="dropdown-divider" />
</li>
<li>
<a
class="dropdown-item js-darkmode-toggle"
href="#"
>{{ _('Toggle dark mode') }}</a>
</li>
<li>
<hr class="dropdown-divider" />
</li>
<p class="m-3 mb-0">
<small>
<a
href="https://openpanel.com/docs/changelog/{{ panel_version }}"
target="_blank"
>
<svg
version="1.0"
style="vertical-align:middle;"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 213.000000 215.000000"
preserveAspectRatio="xMidYMid meet"
>
<g
transform="translate(0.000000,215.000000) scale(0.100000,-0.100000)"
fill="currentColor"
stroke="none"
>
<path
d="M990 2071 c-39 -13 -141 -66 -248 -129 -53 -32 -176 -103 -272 -158 -206 -117 -276 -177 -306 -264 -17 -50 -19 -88 -19 -460 0 -476 0 -474 94 -568 55 -56 124 -98 604 -369 169 -95 256 -104 384 -37 104 54 532 303 608 353 76 50 126 113 147 184 8 30 12 160 12 447 0 395 -1 406 -22 461 -34 85 -98 138 -317 264 -104 59 -237 136 -295 170 -153 90 -194 107 -275 111 -38 2 -81 0 -95 -5z m205 -561 c66 -38 166 -95 223 -127 l102 -58 0 -262 c0 -262 0 -263 -22 -276 -13 -8 -52 -31 -88 -51 -36 -21 -126 -72 -200 -115 l-135 -78 -3 261 -3 261 -166 95 c-91 52 -190 109 -219 125 -30 17 -52 34 -51 39 3 9 424 256 437 255 3 0 59 -31 125 -69z"
/>
</g>
</svg> OpenPanel
<b>{{ panel_version }}</b>
</a>
</small>
</p>
</ul>
</div>
</div>
<!-- /sidebar-footer -->
</aside>
<script type="module">
// this get cleaned up by sco-pe automatically and added to head
let scope = document.getElementById('sidebar-scope');
window.admini.initialize("#sidebar-selector", (el) => {
el.addEventListener("change", (ev) => {
let color = el.selectedOptions[0].style.backgroundColor;
if (color) {
document.querySelector(".sidebar-brand").style.backgroundColor = color;
} else {
document.querySelector(".sidebar-brand").style.backgroundColor = "inherit";
}
});
})
</script>

View File

@@ -0,0 +1,71 @@
{% extends 'base.html' %}
{% block content %}
{% if version %}
<form id="phpiniform" method="POST">
<div id="editor-container">
<textarea id="editor" name="editor_content" rows="40" cols="100">{{ file_content }}</textarea>
</div>
</section>
<footer class="main-footer btn-toolbar" role="toolbar">
<div class="btn-group" role="group" aria-label="Status">
</div>
<div class="ms-auto" role="group" aria-label="Actions">
<button type="submit" class="btn btn-primary">{{ _('Save Changes') }}</button>
</div>
</footer>
</form>
<script>
const editor = document.getElementById('editor');
const codeMirror = CodeMirror.fromTextArea(editor, {
lineNumbers: true,
mode: "text/x-csrc", // Change the mode to match the syntax highlighting for PHP.ini
theme: "dracula", // Change the theme as desired
});
</script>
{% else %}
<p>{{ _('PHP.INI Editor allows you to modify PHP settings stored in the php.ini configuration file, such as memory limits, error reporting, and extension settings.') }}</p>
<form method="GET">
<div class="input-group mb-3">
<label class="input-group-text" for="php_version">{{ _('Select PHP Version:') }}</label>
<select class="form-select form-select-lg" id="php_version" name="version" onchange="redirectToSelectedVersion(this)">
<option value="" disabled selected>{{ _('Select a PHP Version to edit .ini file') }}</option> <!-- Placeholder -->
{% for installed_version in installed_versions %}
<option value="{{ installed_version }}">PHP {{ installed_version }}</option>
{% endfor %}
</select>
</div>
</form>
<script>
function redirectToSelectedVersion(selectElement) {
var selectedVersion = selectElement.value;
if (selectedVersion) {
//window.location.href = `/php/php${selectedVersion}.ini/editor`;
window.open(`/php/php${selectedVersion}.ini/editor`, '_blank');
}
}
</script>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,516 @@
<!-- php/settings.html -->
{% extends 'base.html' %}
{% block content %}
<div class="row">
<div class="container">
<ul class="nav nav-pills nav-fill mb-3" id="ex1" role="tablist">
<li class="active nav-item" role="presentation">
<a data-bs-toggle="tab" href="#versions" class="nav-link active" aria-current="page" href="#"><span class="desktop-only">{{ _("PHP") }} </span>{{ _('versions') }}</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link" data-bs-toggle="tab" href="#extensions"><span class="desktop-only">{{ _("PHP") }} </span>{{ _('extensions') }}</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link" data-bs-toggle="tab" href="#settings" id="settingsLink"><span class="mobile-only">{{ _('default') }}</span><span class="desktop-only">{{ _('Default settings') }} <span class="badge bg-success">{{ _('New') }}</span></span></a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link" id="get_php_options" data-bs-toggle="tab" href="#options"><span class="desktop-only">{{ _("PHP") }} </span>{{ _('options') }}</a>
</li>
</ul>
<div class="tab-content">
<!-- PHP VERSION -->
<div id="versions" class="tab-pane active">
<p>{{ _('Changing the PHP version will stop all the processes on your site. It takes 1-2 seconds to complete. Be sure to check your script and plugin requirements to know which PHP version works best for your website.') }}</p>
<table class="table table-hover">
<thead>
<tr>
<th>{{ _('Domain') }}</th>
<th>{{ _('Current') }} <span class="desktop-only">{{ _('PHP Version') }}</span></th>
<th>{{ _('Change') }} <span class="desktop-only"{{ _('>PHP version for domain') }}</span></th>
</tr>
</thead>
<tbody>
{% for domain in domains %}
<tr>
<td>{{ domain.domain_url }}</td>
<td>
<b style="color:
{% set php_version = php_versions[domain.domain_id] %}
{% if php_version == "/" %}
inherit;
{% else %}
{% set php_version_numeric = php_version | float %}
{% set php_version_integer = php_version_numeric | int %}
{% if php_version_integer >= 8 %}
green;
{% elif php_version_integer >= 7 %}
orange;
{% else %}
red;
{% endif %}
{% endif %}
">
{{ php_version_numeric | string }}
</b>
</td>
<td>
<form method="POST" action="{{ url_for('change_php_version') }}">
<input type="hidden" name="domain_url" value="{{ domain.domain_url }}">
<input type="hidden" name="old_php_version" value="php{{ php_versions[domain.domain_id] }}">
<div class="row">
<div class="col-auto">
<select class="form-select" name="new_php_version" id="new_php_version" {% if php_versions[domain.domain_id] == '/' %}disabled{% endif %}>
{% for version in available_php_versions %}
<option value="{{ version }}" {% if version == 'php' ~ php_versions[domain.domain_id] %}selected{% endif %}>{{ version }}</option>
{% endfor %}
</select></div><div class="col-auto">
<button type="submit" class="btn btn-{% if php_versions[domain.domain_id] == '/' %}secondary disabled{% else %}primary{% endif %} ">{{ _('Change') }} <span class="desktop-only">{{ _('PHP Version') }}</span></button>
</div></div>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- PHP EXTENSIONS -->
<div id="extensions" class="tab-pane fade">
<div class="row g-3" id="php-extensions">
</div>
</div>
<!-- PHP EXTENSIONS -->
<div id="settings" class="tab-pane fade">
<div class="row g-3" id="php-settings">
<div class="col-md-3 col-xl-6">
<div class="card card-one">
<div class="card-header">
<h6 class="card-title">{{ _('Default PHP version:') }}</h6>
</div><!-- card-header -->
<div class="card-body row">
<div class="col">
<p>{{ _('The PHP version that is used by default for newly added domains is the one that you choose here.') }}</p>
<p>{{ _('Current default PHP version:') }} <b>{{ php_default_version|replace('php', '') }}</b></p>
<form method="POST" action="/change_default_php_version_for_new_domains">
<div class="form-group">
<label for="new_php_version">{{ _('Change PHP Version to be used for new domains:') }}</label>
<div class="container">
<div class="row">
<div class="col-sm">
<select class="form-control" id="new_php_version" name="new_php_version">
<option value="" selected disabled>{{ _('Select PHP Version') }}</option>
{% for version in available_php_versions %}
<option value="{{ version }}">{{ version|replace('php', '') }}</option>
{% endfor %}
</select>
</div>
<div class="col-sm">
<button type="submit" class="btn btn-primary">{{ _('Change') }}</button>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="col-md-6 col-xl-6">
<div class="card card-one">
<div class="card-header">
<h6 class="card-title">{{ _('Install a new version') }}</h6>
</div><!-- card-header -->
<div class="card-body row">
<div class="col">
<p>{{ _('For best performance we recommend to only install PHP versions that you will be activelly using.') }}</p>
<p> {{ _("Currently installed PHP versions") }}:
{% for version in available_php_versions %}
<b>{{ version|replace('php', '') }}</b>{% if not loop.last %}, {% endif %}
{% endfor %}
</p>
<label for="php-version-select">{{ _('Select PHP Version to Install:') }}</label>
<div class="container">
<div class="row">
<div class="col-sm">
<form id="install-php-form" method="post" action="/php/install">
<div class="form-group">
<select class="form-control" id="php-version-select" name="php_version">
<option value="" selected disabled>{{ _('Select PHP Version') }}</option>
<ul class="list-unstyled" id="php-available-versions">
</ul>
</select>
</div>
</div>
<div class="col-sm">
<button type="submit" class="btn btn-primary">{{ _('Install') }}</button>
</form>
<script>
document.getElementById('install-php-form').addEventListener('submit', function(event) {
event.preventDefault(); // Prevent default form submission
const phpVersion = document.getElementById('php-version-select').value;
if (!phpVersion) {
alert('Please select a PHP version.');
return;
}
// Create the data to send in the request
const formData = new FormData();
formData.append('php_version', phpVersion);
// Send the form data using Fetch API
fetch('/php/install', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
// Handle the response data (e.g., show a success message)
if (data.success) {
alert('PHP installation started successfully.');
} else {
alert('Failed to start PHP installation: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
//alert('An error occurred while installing PHP.');
});
});
</script>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-12 col-xl-12" id="install_in_progress" style="display: none;">
<div class="card card-one">
<div class="card-header">
<h6 class="card-title">{{ _('PHP version installation log') }}</h6>
</div><!-- card-header -->
<div class="card-body row">
<div class="col">
<pre id="log_progress" style="max-height: 250px;"></pre>
</div>
</div>
</div>
</div>
<script>
$(document).ready(function () {
// Function to check installation progress
function checkInstallationProgress() {
// Check if #settings is in the URL or the link is clicked
if (window.location.hash === "#settings" || window.location.hash === "?set" || $('#settingsLink').hasClass('active')) {
// Perform AJAX request
$.ajax({
url: '/php/check_install',
type: 'GET',
success: function (data) {
try {
// Try to parse the response as JSON
var jsonData = JSON.parse(data);
// Check if it's valid JSON
if (jsonData && typeof jsonData === 'object' && jsonData.message !== undefined) {
// Do nothing if it's JSON with a "message" property
return;
}
} catch (e) {
// If parsing as JSON fails, proceed to display the installation progress
// Update this line to include the logic for styling lines starting with "##"
$('#log_progress').html(data).each(function () {
// Split the content of the <pre> tag into lines
var lines = $(this).text().split('\n');
// Iterate through each line and apply styling if it starts with "##"
for (var i = 0; i < lines.length; i++) {
if (lines[i].trim().startsWith("##")) {
lines[i] = '<b>' + lines[i].trim().substring(2) + '</b>';
}
}
// Join the lines back together and update the content of the <pre> tag
$(this).html(lines.join('\n'));
});
$('#install_in_progress').show();
// Scroll to the bottom of log
var logProgress = document.getElementById('log_progress');
logProgress.scrollTop = logProgress.scrollHeight;
}
},
error: function (error) {
console.error('Error:', error);
// Handle error if needed
}
});
}
}
// Initial check
checkInstallationProgress();
// Set interval to check every 1 second
setInterval(checkInstallationProgress, 1000);
});
</script>
<script>
$(document).ready(function () {
// Send an AJAX request to the Flask endpoint
$.ajax({
type: "GET",
url: "/php-check-available",
dataType: "json",
success: function (data) {
// Handle the response data
if (data.available_for_install && data.available_for_install.length > 0) {
var phpVersions = data.available_for_install;
// Get a reference to the select element
var select = document.getElementById("php-version-select");
// Remove any existing options from the select
select.innerHTML = "";
// Add the default disabled option
var defaultOption = document.createElement("option");
defaultOption.value = "";
defaultOption.text = "{{ _('Select PHP Version') }}";
defaultOption.disabled = true;
defaultOption.selected = true;
select.appendChild(defaultOption);
// Populate the select with PHP versions (stripping "-fpm" and "php" prefix)
for (var i = 0; i < phpVersions.length; i++) {
var version = phpVersions[i].replace(/-fpm$/, '').replace(/^php/, ''); // Removes "-fpm" and "php" prefix
var option = document.createElement("option");
option.value = version;
option.text = version;
select.appendChild(option);
}
} else {
alert("{{ _('No PHP versions found.') }}");
}
},
error: function (xhr, status, error) {
console.error("Error fetching PHP versions:", status, error);
alert("{{ _('Error fetching PHP versions.') }}");
}
});
});
</script>
</div>
</div>
<!-- PHP OPTIONS -->
<div id="options" class="tab-pane fade">
<div class="row g-3" id="php-versions">
</div>
</div>
<script>
$(document).ready(function() {
// Function to fetch and display PHP extensions data
function fetchPhpExtensionsData() {
$.ajax({
url: '/php-extensions',
type: 'GET',
dataType: 'json',
success: function(data) {
// Process the JSON data and display it in the #php-extensions section
var phpExtensionsContainer = $('#php-extensions');
phpExtensionsContainer.empty(); // Clear existing content
// Loop through the PHP versions and their extensions
for (var version in data) {
var extensions = data[version];
// Create a card for each PHP version and its extensions
var cardHtml = '<div class="col-md-6 col-xl-6">' +
'<div class="card card-one">' +
'<div class="card-header">' +
'<h6 class="card-title">PHP ' + version + '</h6>' +
'<nav class="nav nav-icon nav-icon-sm ms-auto">' +
'</nav>' +
'</div><!-- card-header -->' +
'<div class="card-body row">'; // Added "row" class to card-body
// Split extensions into two columns
var splitIndex = Math.ceil(extensions.length / 2);
var firstColumnExtensions = extensions.slice(0, splitIndex);
var secondColumnExtensions = extensions.slice(splitIndex);
// Create the first column
cardHtml += '<div class="col">';
cardHtml += '<ul class="list-unstyled">';
for (var i = 0; i < firstColumnExtensions.length; i++) {
cardHtml += '<li>' + firstColumnExtensions[i] + '</li>';
}
cardHtml += '</ul>';
cardHtml += '</div>'; // Close the first column
// Create the second column
cardHtml += '<div class="col">';
cardHtml += '<ul class="list-unstyled">';
for (var i = 0; i < secondColumnExtensions.length; i++) {
cardHtml += '<li>' + secondColumnExtensions[i] + '</li>';
}
cardHtml += '</ul>';
cardHtml += '</div>'; // Close the second column
cardHtml += '</div></div></div>'; // Close card-body, card, and col-md-6
phpExtensionsContainer.append(cardHtml);
}
},
error: function(error) {
console.log('Error:', error);
}
});
}
// Delay 50ms
setTimeout(fetchPhpExtensionsData, 0);
//setInterval(fetchPhpExtensionsData, 1000);
});
// Function to fetch PHP options and populate the tab
function fetchPhpOptions() {
// Make an Ajax request to fetch data from the /php-limits endpoint
$.ajax({
url: '/php-limits',
type: 'GET',
dataType: 'json',
success: function(data) {
// Process the JSON data and create a card for each PHP version
var phpVersionsContainer = $('#php-versions');
phpVersionsContainer.empty(); // Clear existing content
for (var version in data) {
var phpConfiguration = data[version];
// Create a card for each PHP version
var cardHtml = '<div class="col-md-6 col-xl-6">' +
'<div class="card card-one">' +
'<div class="card-header">' +
'<h6 class="card-title">PHP ' + version + '</h6>' +
'<nav class="nav nav-icon nav-icon-sm ms-auto">' +
'<a href="/php/php' + version + '.ini/editor" target="_blank" class="nav-link"><i class="bi bi-pencil-fill"></i> &nbsp; {{ _("Edit PHP.INI") }}</a>' +
'</nav>' +
'</div><!-- card-header -->' +
'<div class="card-body"><table class="table">' +
'<thead><tr><th>{{ _("Setting") }}</th><th>Value</th></tr></thead><tbody>';
// Loop through the PHP configuration settings for this version
for (var setting in phpConfiguration) {
cardHtml += '<tr><td>' + setting + '</td><td>' + phpConfiguration[setting] + '</td></tr>';
}
cardHtml += '</tbody></table></div></div></div>';
phpVersionsContainer.append(cardHtml);
}
},
error: function(error) {
console.log('Error:', error);
}
});
}
setTimeout(fetchPhpOptions, 0);
//setInterval(fetchPhpOptions, 1000);
// Attach the click event handler to the tab link
$('#get_php_options').on('click', function() {
// Fetch the PHP options and populate the tab
fetchPhpOptions();
});
</script>
</div>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", function() {
// Get the hash value from the URL
var hash = window.location.hash;
// Check if the hash corresponds to any of the tab links
var tabLinks = document.querySelectorAll('.nav-link[data-bs-toggle="tab"]');
for (var i = 0; i < tabLinks.length; i++) {
var tabLink = tabLinks[i];
var href = tabLink.getAttribute("href");
// Check if the hash matches the href
if (hash === href) {
// Trigger a click event on the matching tab link
tabLink.click();
break; // Stop checking once a match is found
}
}
});
</script>
{% endblock %}

535
templates/admini/pm2.html Normal file
View File

@@ -0,0 +1,535 @@
{% extends 'base.html' %}
{% block content %}
{% if domains %}
<style>
@keyframes rotate {
to {
--angle: 360deg
}
}
@property --angle {
syntax: "<angle>";
initial-value: 0deg;
inherits: false
}
.CrawlerStatusCard.active {
animation: rotate 2s linear infinite;
background: hsla(0,0%,100%,.5);
border: 2px solid transparent;
border-image: conic-gradient(from var(--angle),transparent 0deg 90deg,transparent 90deg 180deg,transparent 180deg 270deg,#0077bc 270deg 1turn) 1 stretch;
}
</style>
<div class="modal fade" id="viewModal" tabindex="-1" role="dialog" aria-labelledby="viewModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="viewModalLabel">{{ _('Application Logs') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ _('Close') }}"></button>
</div>
<div class="modal-body bg-dark">
<div id="fileContent" class="mb-0"></div>
</div>
</div>
</div>
</div>
<script>
$(document).ready(function() {
$("#port").on('input', function() {
// Get the user-input port value
var portValue = $(this).val();
// Make an AJAX request to check if the port is in use
$.ajax({
url: "/json/check_if_port_is_in_use",
type: "POST",
data: {port: portValue},
success: function(data) {
// Display the appropriate symbol and message based on the response
var resultDiv = $("#port-check-result");
if (data.message.includes("not in use")) {
resultDiv.html('<i class="bi bi-check-lg" style="color:green;"></i> {{ _("available") }}');
} else {
resultDiv.html('<i class="bi bi-x-lg" style="color:red;"></i> {{ _("already in use") }}');
}
},
error: function(data) {
// Handle the error here if needed
console.log("Error:", data);
}
});
});
$("#startup_file").on('input', function() {
// Get the user-input file value
var fileValue = $(this).val();
// Make an AJAX request to check if the file is in use
$.ajax({
url: "/json/check_if_file_exists",
type: "POST",
data: {file: fileValue},
success: function(data) {
// Display the appropriate symbol and message based on the response
var resultDiv = $("#file-check-result");
if (data.message.includes("exists")) {
resultDiv.html('<i class="bi bi-check-lg" style="color:green;"></i> {{ _("file exists") }}');
} else {
resultDiv.html('<i class="bi bi-x-lg" style="color:red;"></i> {{ _("file does not exist!") }}');
}
},
error: function(data) {
// Handle the error here if needed
console.log("Error:", data);
}
});
});
});
</script>
<div class="collapse" id="collapseExample">
<div id="toAddActive" class="card CrawlerStatusCard card-body">
<!-- Form for adding new applications -->
<div class="container">
<a href="#" class="nije-link" id="cancelLink" style="display: none;"><i class="bi bi-x-lg" style="right: 15px;top: 15px;position: absolute;color: black;padding: 6px 10px;border-radius: 50px;"></i></a>
<div class="row">
<div class="col-md-6 offset-md-3">
<h2 class="mb-3"><i class="bi bi-app-indicator"></i> {{ _('Create a new Application') }}</h2>
<p>{{ _("Run a NodeJS or Python application.") }}</p>
<form method="post" id="installpm2" action="/pm2/install">
<div class="form-group mb-2">
<label for="domain_id">{{ _('Application URL:') }}</label>
<div class="input-group">
<select class="form-control" name="domain_id" id="domain_id">
{% for domain in domains %}
<option class="punycode" value="{{ domain.domain_id }}">{{ domain.domain_url }}</option>
{% endfor %}
</select>
<div class="input-group-append">
<input type="text" class="form-control" name="subdirectory" id="subdirectory" placeholder="{{ _('subfolder (optional)') }}">
</div>
</div>
</div>
<div class="form-group">
<label for="admin_email">{{ _('Application Startup file:') }}</label>
<div class="input-group mb-0">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text" id="full-path">/home/{{ current_username }}/</span>
</div>
<input type="text" class="form-control" name="startup_file" id="startup_file" required="">
</div>
<div id="file-check-result">&nbsp;</div>
</div>
</div>
<div class="form-group">
<label for="flags">{{ _('Optional flags:') }}</label>
<div class="input-group mb-0">
<input type="text" class="form-control" name="flags" id="flags">
</div>
</div>
<div class="form-group row mt-2">
<div class="col-md-6">
<label for="app_type">{{ _('Type:') }}</label>
<select class="form-select select2" name="app_type" id="app_type">
<option value="node">{{ _("NodeJS") }}</option>
<option value="python3">{{ _("Python") }}</option>
</select>
</div>
<!--div class="col-md-6">
<label for="version">Version:</label>
<input type="text" class="form-control" name="version" id="version" value="">
</div-->
<div class="col-md-6">
<label for="website_name">{{ _("Port:") }}</label>
<input type="text" class="form-control" name="port" id="port" value="" required>
<div id="port-check-result">&nbsp;</div>
</div>
</div>
<div class="form-group row mt-2 mb-2">
<div class="col-md-6">
<label for="watch">{{ _('Watch:') }}</label>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="flexSwitchCheckChecked" checked>
<label class="form-check-label" for="flexSwitchCheckChecked">{{ _('Automatically restart app on file changes') }}</label>
</div>
</div>
<div class="col-md-6">
<label for="logs">{{ _('Enable logs:') }}</label>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="flexSwitchCheckChecked" checked>
<label class="form-check-label" for="flexSwitchCheckChecked">{{ _('Collect logs') }}</label>
</div>
</div>
</div>
<script>
$(document).ready(function() {
// Check if the URL contains the parameter "install"
const urlParams = new URLSearchParams(window.location.search);
const installParam = urlParams.get('install');
if (installParam || window.location.hash === '#install') {
// Show the Bootstrap collapsible element
$("#collapseExample").collapse('show');
}
// Add event listener to the "Install website" button to toggle form and jumbotron
$("#collapseExample").on('shown.bs.collapse', function () {
$("#jumbotronSection").hide();
$("#cancelLink").show();
});
// Add event listener to the "Cancel" link to toggle form and jumbotron
$("#cancelLink").click(function() {
$("#collapseExample").collapse('hide');
$("#jumbotronSection").show();
$("#cancelLink").hide();
});
});
</script>
<br>
<button type="submit" class="btn btn-primary" id="installpm2Button">{{ _('Create') }}</button>
</form>
<div id="statusMessage"></div>
<script>
document.getElementById("installpm2").addEventListener("submit", function(event) {
event.preventDefault();
// Hide the installation form
const installForm = document.getElementById("installpm2");
installForm.style.display = "none"; // Hide the form
// Show the active state
document.getElementById("toAddActive").classList.add("active");
// Disable the button to prevent multiple clicks
const button = document.getElementById("installpm2Button");
button.disabled = true;
button.innerText = "Installing..."; // Initial message
// Clear previous status messages
const statusMessageDiv = document.getElementById("statusMessage");
statusMessageDiv.innerText = "";
// Start fetching for updates
const formData = new FormData(this); // Get the form data
const responseStream = fetch('/pm2/install', {
method: 'POST',
body: formData,
});
responseStream
.then(response => {
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
function read() {
reader.read().then(({ done, value }) => {
if (done) {
showCompletion();
return;
}
const message = decoder.decode(value, { stream: true });
const jsonMessages = message.split('\n').filter(Boolean); // Split lines and remove empty
jsonMessages.forEach(jsonMessage => {
try {
const data = JSON.parse(jsonMessage);
if (data.status) {
statusMessageDiv.innerText += data.status + "\n"; // append
} else if (data.error) {
toaster({
body: data.error,
className: 'border-0 text-white bg-danger',
});
showCompletion(); // redirect
}
} catch (e) {
//console.error("Error parsing JSON:", e);
}
});
read(); // Continue
});
}
read(); // Start
})
.catch(error => {
statusMessageDiv.innerText += "Error occurred while processing.\n";
console.error("Fetch error:", error);
toaster({
body: error,
className: 'border-0 text-white bg-danger',
});
showCompletion(); // redirect
});
function showCompletion() {
statusMessageDiv.style.display = "block";
window.location.href = '/pm2';
}
});
</script>
</div>
</div>
</div>
</div></div>
{% if pm2_data %}
<div class="table-responsive">
<table class="table table-bordered">
<thead class="table-dark">
<tr>
<!--th>ID</th-->
<th><span data-bs-toggle="tooltip" data-bs-placement="top" title="{{ _('Application / Domain Name.') }}">{{ _('Application') }}</span></th>
<!--th><span data-bs-toggle="tooltip" data-bs-placement="top" title="NodeJS/Python version the application is using.">Version</span></th-->
<th>{{ _('Uptime') }}</th>
<th>{{ _('Restarts') }}</th>
<th><span data-bs-toggle="tooltip" data-bs-placement="top" title="{{ _('Current status of the application process.') }}">{{ _('Status') }}</span></th>
<th>{{ _('CPU') }}</th>
<th>{{ _('Memory') }}</th>
<th><span data-bs-toggle="tooltip" data-bs-placement="top" title="{{ _('If watching is enabled, the application will automatically restart when its files are edited.') }}">{{ _('Watching') }}</span></th>
<th>Actions</th>
<!-- New columns for data from the database -->
<th><span data-bs-toggle="tooltip" data-bs-placement="top" title="NodeJS/Python">{{ _('Type') }}</span></th>
<th><span data-bs-toggle="tooltip" data-bs-placement="top" title="{{ _('Port that the application is using.') }}">Port</span></th>
<th><span data-bs-toggle="tooltip" data-bs-placement="top" title="{{ _('Path to the application startup file (app.js)') }}"> {{ _('Startup file') }}</span></th>
</tr>
</thead>
<tbody>
{% if pm2_data %}
{% for row in pm2_data %}
<tr>
<!--td>{{ row[0] }}</td-->
<td>{{ row[1] }}</td>
<!--td>{{ row[3] }}</tdVERSION-->
<td>{{ row[6] }}</td>
<td>{{ row[7] }}</td>
<td class="{% if row[8] == 'stopped' %}text-danger{% elif row[8] == 'online' %}text-success{% else %}text-warning{% endif %}">
{{ row[8] }}
</td>
<td>{{ row[9] }}</td>
<td>{{ row[10] }}</td>
<td>{{ row[12] }}</td>
<td>
{% if row[8] == 'stopped' %}
<form action="/pm2/start/{{ row[1] }}" method="post" style="display: inline;">
<button type="submit" class="btn btn-success btn-sm"><i class="bi bi-play-fill"></i> {{ _('Start') }}</button>
</form>
{% else %}
<form action="/pm2/stop/{{ row[1] }}" method="post" style="display: inline;">
<button type="submit" class="btn btn-warning btn-sm"><i class="bi bi-stop-fill"></i> {{ _('Stop') }}</button>
</form>
{% endif %}
<form action="/pm2/restart/{{ row[1] }}" method="post" style="display: inline;">
<button type="submit" class="btn btn-primary btn-sm"><i class="bi bi-arrow-clockwise"></i> {{ _('Restart') }}</button>
</form>
<form action="/pm2/delete/{{ row[1] }}" method="post" style="display: inline;">
<button class="btn btn-danger btn-sm" type="button" style="display: inline;" onclick="confirmAppDelete(this);"><i class="bi bi-trash3"></i> {{ _('Delete') }}</button>
</form>
<button class="btn btn-sm btn-transparent view-button" data-file="{{ row[1] }}" data-type="logs">{{ _('Logs') }}</button>
</td>
<!-- Data from the database for the corresponding site -->
{% for site in all_sites %}
{% if row[1] == site.site_name %}
<td>
{% if site.type == "NodeJS" %}
<i class=""><img src="/static/images/icons/nodejs.png" style="height: 25px;"></i>
{% elif site.type == "Python" %}
<i class=""><img src="/static/images/icons/python.png" style="height: 25px;"></i>
{% else %}
{{ site.type }}
{% endif %}
</td>
<td>{{ site.ports }}</td>
<td>{{ site.path }}</td>
{% endif %}
{% endfor %}
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="10">{{ _('No PM2 data available.') }}</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
{% else %}
<!-- Display jumbotron for no installations -->
<div class="jumbotron text-center mt-5" id="jumbotronSection">
<h1>{{ _('No existing Applications') }}</h1>
<p>{{ _('There are no existing applications. You can start a new NodeJS or Python app below.') }}</p>
<button class="btn btn-lg btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#collapseExample" aria-expanded="false" aria-controls="collapseExample">
<i class="bi bi-app-indicator"></i>{{ _(' Create Application') }}
</button>
</div>
{% endif %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/punycode/2.1.1/punycode.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
var punycodeElements = document.querySelectorAll(".punycode");
punycodeElements.forEach(function(element) {
element.textContent = punycode.toUnicode(element.textContent);
});
});
</script>
</section>
<footer class="main-footer btn-toolbar" role="toolbar">
<div class="btn-group" role="group" aria-label="Status">
</div>
<div class="ms-auto" role="group" aria-label="Actions">
<button class="btn btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#collapseExample" aria-expanded="false" aria-controls="collapseExample">
<i class="bi bi-plus-lg"></i> {{ _('New') }}<span class="desktop-only"> {{ _('Application') }}</span>
</button>
</div>
</footer>
<script>
function confirmAppDelete(button) {
var countdown = 5;
var countdownActive = true; // Variable to track countdown status
// Change the button style and text
$(button).removeClass('btn-danger').addClass('btn-dark').html('<i class="bi bi-trash3-fill"></i> Confirm <span class="btn-indicator btn-indicator-mini bg-danger">' + countdown + '</span>');
// Interval to update countdown
var intervalId = setInterval(function () {
countdown--;
// Update the countdown value in the button text
$(button).find('.btn-indicator-mini').text(countdown);
// Remove the onclick event to prevent further changes on subsequent clicks
$(button).removeAttr('onclick');
// If countdown reaches 0, revert the button, clear the interval, and set countdownActive to false
if (countdown === 0) {
clearInterval(intervalId);
revertButton(button);
countdownActive = false;
}
}, 1000);
// Add a click event to the confirm button
$(button).on('click', function () {
// Check if countdown is active before allowing form submission
if (countdownActive) {
// Submit the parent form when the button is clicked during the countdown
$(button).closest('form').submit();
}
});
}
// Function to revert the button to its initial state
function revertButton(button) {
$(button).removeClass('btn-dark').addClass('btn-danger').html('<i class="bi bi-trash3"></i> Delete');
$(button).attr('onclick', 'confirmAppDelete(this);');
}
</script>
<script>
$('html').on('click', '.view-button', function() {
const button = $(this);
const domain = button.data('file');
fetch(`/pm2/logs/${encodeURIComponent(domain)}`)
.then(response => response.text())
.then(data => {
const modalTitle = $('#viewModalLabel');
const modalBody = $('#viewModal').find('.modal-body');
modalTitle.text("Error log for application: " + domain);
modalBody.empty();
const textContent = document.createElement('pre');
textContent.textContent = data;
modalBody.append(textContent);
$('#viewModal').modal('show');
})
.catch(error => {
console.error('{{ _("Error fetching pm2 logs for applicaiton:") }}', error);
});
});
</script>
{% else %}
<!-- Display jumbotron for no domains -->
<div class="jumbotron text-center mt-5" id="jumbotronSection">
<h1>{{ _("No Domains") }}</h1>
<p>{{ _("Add a domain name first in order to install Python adn NodeJS applications.") }}</p>
<a href="/domains#add-new" class="btn btn-lg btn-primary">
<i class="bi bi-plus-lg"></i> {{ _("Add a Domain Name") }}
</a>
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,114 @@
{% extends 'base.html' %}
{% block content %}
<div class="container mt-4">
{% if error_message %}
<div class="alert alert-danger mt-3" role="alert">
{{ error_message }}
</div>
{% else %}
<table class="table table-bordered mt-3">
<thead>
<tr>
<th>PID</th>
<th>{{ _('TIME') }}</th>
<th>CPU</th>
<th>CMD</th>
<th>{{ _('ACTION') }}</th>
</tr>
</thead>
<tbody>
{% for process in process_data %}
{% if '/etc/entrypoint.sh' not in process['CMD'] and 'ps -eo pid,%cpu,time,cmd' not in process['CMD'] and '/dev/null' not in process['CMD'] %}
{# Display the current process #}
<tr>
<td>{{ process['PID'] }}</td>
<td>{{ process['TIME'] }}</td>
<td>{{ process['%CPU'] }}</td>
<td>
{% if process['CMD']|length > 255 %}
<!-- Display shortened CMD with link to show full CMD -->
<span id="short_cmd_{{ process['PID'] }}">
{{ process['CMD'][:255] }}...
<a href="#" onclick="showFullCmd('{{ process['PID'] }}');">{{ _('View full command') }}</a>
</span>
<span id="full_cmd_{{ process['PID'] }}" style="display:none;">
{{ process['CMD'] }}
</span>
{% else %}
<!-- Display full CMD if it's not too long -->
{{ process['CMD'] }}
{% endif %}
</td>
<td><button class="btn btn-danger" type="button" onclick="sendPID('{{ process['PID'] }}');">kill</button></td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
<script>
function showFullCmd(pid) {
// Hide shortened CMD and show full CMD
$('#short_cmd_' + pid).hide();
$('#full_cmd_' + pid).show();
}
</script>
<script>
// Function to initiate the process kill when the "Kill" button is clicked
function sendPID(pid) {
const requestData = {
pid_to_kill: pid,
};
const toast = toaster({
body: 'Terminating PID: '+ pid,
className: 'border-0 text-white bg-primary',
});
fetch('/process-manager', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestData),
})
.then(response => {
if (!response.ok) {
throw new Error('{{ _("Network response was not ok") }}');
}
return response.json(); // Use response.json() to parse JSON
})
.then(data => {
console.log('PID sent successfully');
// Access data as a JSON object here
})
.catch(error => {
console.error('Sending PID failed:', error);
// Display an error message
const toast = toaster({
body: '{{ _("Killing PID:") }} '+ pid +' {{ _("failed") }}',
className: 'border-0 text-white bg-error',
});
})
.finally(() => {
// Display a success message
const toast = toaster({
body: 'PID: '+ pid +' {{ _("terminated") }}',
className: 'border-0 text-white bg-success',
});
});
}
</script>
{% endblock %}

321
templates/admini/redis.html Normal file
View File

@@ -0,0 +1,321 @@
<!-- redis.html -->
{% extends 'base.html' %}
{% block content %}
<script type="module">
// Function to attach event listeners
function attachEventListeners() {
document.querySelectorAll("button[type='submit']").forEach((btn) => {
if (!btn.classList.contains("limits")) {
btn.addEventListener("click", async (ev) => {
ev.preventDefault();
const action = btn.closest("form").querySelector("input[name='action']").value;
let btnClass, toastMessage;
if (action === 'enable') {
btnClass = 'success';
toastMessage = "{{ _('Enabling REDIS service..') }}";
} else if (action === 'install_redis') {
btnClass = 'primary';
toastMessage = '{{ _("Installing REDIS service.. Please wait") }}';
} else if (action === 'disable') {
btnClass = 'danger';
toastMessage = "{{ _('Disabling REDIS service..') }}";
}
const toast = toaster({
body: toastMessage,
className: `border-0 text-white bg-${btnClass}`,
});
try {
const response = await fetch(window.location.href, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `action=${action}`,
});
// get the response HTML content
const resultHtml = await response.text();
// Parse the HTML string to extract the content of the specific element
const parser = new DOMParser();
const doc = parser.parseFromString(resultHtml, 'text/html');
const mainScopeContent = doc.getElementById("main-scope")?.innerHTML;
// Replace the content of the element with the ID "main-scope"
const mainScopeElement = document.getElementById("main-scope");
if (mainScopeElement) {
mainScopeElement.innerHTML = mainScopeContent || '';
}
// Reattach event listeners after updating content
attachEventListeners();
// Reinitialize the log script
initializeLogScript();
} catch (error) {
console.error('Error:', error);
}
});
}
});
}
// Function to initialize the log script
function initializeLogScript() {
$(document).ready(function() {
$("#service-log").click(function(event) {
event.preventDefault();
$.ajax({
url: "/view-log/var/log/redis/redis-server.log",
type: "GET",
success: function(data) {
$("#log-content").html(data);
$("#log-container").show(); // Show the container when data is fetched
},
error: function() {
$("#log-content").html("Error fetching log content.");
$("#log-container").show(); // Show the container even on error
}
});
});
});
}
// Attach event listeners initially
attachEventListeners();
</script>
<div class="row g-3">
{% if redis_status_display == 'NOT INSTALLED' %}
<div class="jumbotron text-center mt-5" id="jumbotronSection">
<h1><i class="bi bi-x-lg" style="color:red;"></i> {{_('REDIS is not currently installed.')}}</h1>
<p>{{_('To install REDIS click on the button bellow.')}}</p>
<form method="post">
<input type="hidden" name="action" value="install_redis">
<button class="btn btn-lg btn-primary" type="submit">{{_('INSTALL REDIS')}}</button>
</form>
</div>
{% elif redis_status_display == 'ON' %}
{% set maxmemory_value_int = maxmemory_value | int %}
{% set maxmemory_value_in_MB = maxmemory_value_int / (1000 * 1000) %}
<script>
function updateSliderValue(value) {
const allowedValues = [128, 256, 512, 1024, 2048];
const closestValue = allowedValues.reduce((prev, curr) =>
Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev
);
document.getElementById('set_memory').value = closestValue;
document.getElementById('slider_value').textContent = closestValue + ' MB';
}
</script>
<div class="col-md-4 col-xl-4">
<div class="card card-one">
<div class="card-header">
<h6 class="card-title">{{ _('Connection Info') }}</h6>
</div>
<div class="card-body">
<div class="row mt-2 mb-2">
<label class="card-title fw-medium text-dark mb-1">{{ _('status:') }}</label><div class="col-6">
<h3 class="card-value mb-1"><i class="bi bi-check-circle-fill"></i> {{ _('Active') }}</h3>
</div><!-- col -->
</div>
<hr>
<div class="row mt-2 mb-2">
<div class="col-6">
<label class="card-title fw-medium text-dark mb-1">{{ _('REDIS server:') }}</label><h3 class="card-value mb-1">127.0.0.1</h3>
<span class="d-block text-muted fs-11 ff-secondary lh-4">{{ _('*or localhost') }}</span>
</div><!-- col -->
</div>
<hr>
<div class="row mt-2 mb-2">
<div class="col-12">
<label class="card-title fw-medium text-dark mb-1">{{ _('Port:') }}</label>
<h3 class="card-value mb-1">6379</h3><span class="d-block text-muted fs-11 ff-secondary lh-4">{{ _('*Access to the service is NOT available from other servers.') }}</span>
</div><!-- col -->
</div><!-- row -->
</div><!-- card-body -->
<div class="card-footer d-flex justify-content-center">
<a href="#" class="fs-sm" id="service-log">{{ _('View Redis service Log') }}</a>
</div>
</div><!-- card-one -->
</div>
<div class="col-md-6 col-xl-8">
<div class="card card-one">
<div class="card-header">
<h6 class="card-title">{{ _('REDIS Memory Allocation') }}</h6>
<nav class="nav nav-icon nav-icon-sm ms-auto">
<a href="" class="nav-link"><i class="ri-refresh-line"></i></a>
<a href="" class="nav-link"><i class="ri-more-2-fill"></i></a>
</nav>
</div><!-- card-header -->
<div class="card-body">
<p class="mb-3 fs-xs">{{ _('You can allocate RAM to REDIS service.') }}</p>
<form method="post">
<div class="card p-3 d-flex flex-row mb-2">
<div class="card-icon"><img src="{{ url_for('static', filename='images/icons/redis.png') }}" style="width: 50px;"></div>
<div class="ms-3" style="width: 100%;">
<p class="fs-xs text-secondary mb-0 lh-4">{{ _('Current Memory limit for REDIS service') }}</p>
<h4 class="card-value mb-1">
{% if maxmemory_value_int == 0 %}
<i class="bi bi-infinity"></i>
{% else %}
{{ maxmemory_value_in_MB | int }} MB
{% endif %}
</h4>
<label class="card-title fw-medium text-dark mt-5 mb-1">{{ _('Change Memory Allocation') }}</label>
<div class="form-group">
<div class="form-group">
<input type="range" style="width: 80%;" class="form-control-range" id="set_memory" name="set_memory"
min="128" max="2048" step="128" value="{{ maxmemory_value_in_MB | int }}" oninput="updateSliderValue(this.value)">
<span id="slider_value">{{ maxmemory_value_in_MB | int }} MB</span><br>
<button type="submit" class="limits btn text-right btn-primary mt-3">{{ _('Save') }}</button>
</div>
</div>
</form> </div>
</div>
</div><!-- col -->
</div><!-- row -->
</div>
<div class="row g-3">
<div class="col-md-6 col-xl-12" style="display: none;" id="log-container">
<div class="card card-one">
<div class="card-header">
<h6 class="card-title">{{ _('REDIS service logs') }}</h6>
<nav class="nav nav-icon nav-icon-sm ms-auto"></nav>
</div><!-- card-header -->
<div class="card-body">
<pre id="log-content"></pre>
</div><!-- card-body -->
</div><!-- card -->
</div>
</div>
{% endif %}
{% if redis_status_display == 'ON' %}
{% elif redis_status_display == 'NOT INSTALLED' %}
{% elif redis_status_display == 'OFF' %}
<div class="jumbotron text-center mt-5" id="jumbotronSection">
<h1><i class="bi bi-x-lg" style="color:red;"></i> {{ _('REDIS is currently disabled.') }}</h1>
<p>{{ _('To enable REDIS SERVER click on the button bellow.') }}</p>
<form method="post">
<input type="hidden" name="action" value="enable">
<button class="btn btn-lg btn-primary" type="submit">{{ _('START REDIS') }}</button>
</form>
</div>
{% else %}
<div class="jumbotron text-center mt-5" id="jumbotronSection">
<h1><i class="bi bi-x-lg"></i> {{ _('REDIS service status is unknown.') }}</h1>
<p>{{ _('Unable to determinate current REDIS service status, try Start&Stop actions.') }} <br>{{ _('If the issue persists please contact support.') }}</p>
<form method="post">
<input type="hidden" name="action" value="enable">
<button class="btn btn-primary d-flex align-items-center gap-2" type="submit">{{ _('START') }}</button>
</form>
<form method="post">
<input type="hidden" name="action" value="disable">
<button class="btn btn-primary d-flex align-items-center gap-2" type="submit">{{ _('STOP') }}</button>
</form>
</div>
{% endif %}
</div>
<script>
$(document).ready(function() {
$("#service-log").click(function(event) {
event.preventDefault();
$.ajax({
url: "/view-log/var/log/redis/redis-server.log",
type: "GET",
success: function(data) {
$("#log-content").html(data);
$("#log-container").show(); // Show the container when data is fetched
},
error: function() {
$("#log-content").html("Error fetching log content.");
$("#log-container").show(); // Show the container even on error
}
});
});
});
</script>
</section>
<footer class="main-footer btn-toolbar" role="toolbar">
<div class="btn-group" role="group" aria-label="Status">
<label>status:</label><b> {% if redis_status_display == 'ON' %} Enabled{% elif redis_status_display == 'OFF' %} Disabled{% elif redis_status_display == 'NOT INSTALLED' %} Not Installed{% else %} Unknown{% endif %}</b>
</div>
<div class="ms-auto" role="group" aria-label="Actions">
{% if redis_status_display == 'ON' %}
<form method="post">
<input type="hidden" name="action" value="disable">
<button class="btn btn-danger d-flex align-items-center gap-2" type="submit">{{ _('Disable REDIS service') }}</button>
</form>
{% elif redis_status_display == 'OFF' %}
<form method="post">
<input type="hidden" name="action" value="enable">
<button class="btn btn-success d-flex align-items-center gap-2" type="submit">{{ _('Enable REDIS service') }}</button>
</form>
{% elif redis_status_display == 'NOT INSTALLED' %}
<form method="post">
<input type="hidden" name="action" value="install_redis">
<button class="btn btn-success d-flex align-items-center gap-2" type="submit">{{_('Install REDIS')}}</button>
</form>
{% else %}
<form method="post">
<input type="hidden" name="action" value="disable">
<button class="btn btn-primary d-flex align-items-center gap-2" type="submit">{{ _('Disable REDIS service') }}</button>
</form>
<form method="post">
<input type="hidden" name="action" value="enable">
<button class="btn btn-primary d-flex align-items-center gap-2" type="submit">{{ _('Enable REDIS service') }}</button>
</form>
{% endif %}
</div>
</footer>
{% endblock %}

View File

@@ -0,0 +1,68 @@
{% extends 'base.html' %}
{% block content %}
<style>
a {
text-decoration: none;
}
</style>
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>{{ _('Site Name') }}</th>
<th>{{ _('Admin Email') }}</th>
<th>{{ _('Created on') }}</th>
<th>{{ _('Type') }}</th>
<th>{{ _('Version') }}</th>
<th>{{ _('Actions') }}</th>
</tr>
</thead>
<tbody>
{% for site in data %}
<tr>
<td><img src="https://www.google.com/s2/favicons?domain={{ site[0] }}" alt="{{ site[0] }} Favicon" style="width:16px; height:16px; margin-right:5px;">{{ site[0] }}</td>
<td>{{ site[2] }}</td>
<td>{{ site[4] }}</td>
<td>{{ site[5] }}</td>
<td>{{ site[3] }}</td>
<td>
{% if 'Static' in site[5] %}
<a href="/files/{{ site[0] }}" class="btn btn-primary"> Manage</a>
{% elif 'WordPress' in site[5] or 'Node' in site[5] or 'Mautic' in site[5] %}
<a href="/website?domain={{ site[0] }}" class="btn btn-primary"> Manage</a>
{% else %}
<a href="/website?domain={{ site[0] }}" class="btn btn-primary"> Manage</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
<footer class="main-footer btn-toolbar" role="toolbar">
<div class="btn-group" role="group" aria-label="Status">
</div>
<div class="ms-auto" role="group" aria-label="Actions">
<button class="btn btn-primary mx-2" data-bs-toggle="modal" data-bs-target="#cardModal"><i class="bi bi-plus-lg"></i>{{ _('New') }}<span class="desktop-only"> {{ _('Website') }}</span>
</button>
</div>
</footer>
{% endblock %}

214
templates/admini/ssh.html Normal file
View File

@@ -0,0 +1,214 @@
<!-- SSH.html -->
{% extends 'base.html' %}
{% block content %}
<script type="module">
// Function to attach event listeners
function attachEventListeners() {
document.querySelectorAll("button[type='submit']").forEach((btn) => {
btn.addEventListener("click", async (ev) => {
ev.preventDefault();
const action = btn.closest("form").querySelector("input[name='action']").value;
let btnClass, toastMessage, requestBody;
if (action === 'enable') {
btnClass = 'success';
toastMessage = "{{ _('Enabling SSH access..') }}";
requestBody = `action=${action}`;
} else if (action === 'disable') {
btnClass = 'danger';
toastMessage = "{{ _('Disabling SSH access..') }}";
requestBody = `action=${action}`;
} else if (action === 'change_password') {
btnClass = 'success';
toastMessage = "{{ _('Password changed successfully.') }}";
// Close the Bootstrap modal programmatically
const modalElement = new bootstrap.Modal(document.getElementById('changePasswordModal'));
modalElement.hide();
// Remove the modal backdrop
const modalBackdrop = document.querySelector('.modal-backdrop');
if (modalBackdrop) {
modalBackdrop.remove();
}
// Get the new password value
const newPassword = btn.closest("form").querySelector("#new_password").value;
// Include the new password in the request body
requestBody = `action=${action}&new_password=${encodeURIComponent(newPassword)}`;
}
const toast = toaster({
body: toastMessage,
className: `border-0 text-white bg-${btnClass}`,
});
try {
const response = await fetch(window.location.href, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: requestBody,
});
// get the response HTML content
const resultHtml = await response.text();
// Parse the HTML string to extract the content of the specific element
const parser = new DOMParser();
const doc = parser.parseFromString(resultHtml, 'text/html');
const mainScopeContent = doc.getElementById("main-scope")?.innerHTML;
// Replace the content of the element with the ID "main-scope"
const mainScopeElement = document.getElementById("main-scope");
if (mainScopeElement) {
mainScopeElement.innerHTML = mainScopeContent || '';
}
// Reattach event listeners after updating content
attachEventListeners();
} catch (error) {
console.error('Error:', error);
}
});
});
}
// Attach event listeners initially
attachEventListeners();
</script>
<div class="col-12">
{% if ssh_status_display == 'ON' %}
{% elif ssh_status_display == 'OFF' %}
<h4 class="text-center mb-0"><i class="bi bi-shield-slash-fill" style="color:green;"></i> {{ _('SSH access is currently disabled.') }}</h4>
{% else %}
<div class="alert alert-warning text-center" role="alert">
<h4 class="mb-0"><i class="bi bi-shield-shaded"></i> {{ _('SSH service status is unknown. Contact Administrator.') }}</h4>
</div>
{% endif %}
</div>
{% if ssh_status_display == 'ON' %}
<div class="row g-3">
<div class="col-md-6 col-xl-12">
<div class="card card-one">
<div class="card-header">
<h6 class="card-title">{{ _('SSH Connection Information') }}</h6>
</div><!-- card-header -->
<div class="card-body">
<div class="row g-4">
<div class="col">
<label class="card-label fs-sm fw-medium mb-1">{{ _('IP address:') }}</label>
<h2 class="card-value mb-0">{{ server_ip }}</h2>
</div><!-- col -->
<div class="col-5 col-sm">
<label class="card-label fs-sm fw-medium mb-1">{{ _('SSH Port:') }}</label>
<h2 class="card-value mb-0">{% if host_port_mapping %} <b><code>{{ host_port_mapping }}</code></b> {% else %} {{ _("CONTAINER NOT RUNNING") }} {% endif %}</h2>
</div><!-- col -->
<div class="col">
<label class="card-label fs-sm fw-medium mb-1">{{ _('Username:') }}</label>
<h2 class="card-value mb-0">{{ current_username }}</h2>
</div><!-- col -->
<div class="col">
<label class="card-label fs-sm fw-medium mb-1">{{ _('Password:') }}</label>
<h2 class="card-value mb-0">**********</h2>
<button class="btn btn-outline" id="change_ssh_password_modal_button" type="button" data-bs-toggle="modal" data-bs-target="#changePasswordModal">{{ _('Change SSH Password') }}</button></div>
</div>
</div>
<div class="card-footer">
<div class="text-center"><pre class="mb-0">
ssh {{current_username}}@{{ server_ip }} -p {{ host_port_mapping }}
</pre>
</div>
</div>
</div>
</div><!-- col -->
</div>
<!-- Modal for changing password -->
<div class="modal fade" id="changePasswordModal" tabindex="-1" role="dialog" aria-labelledby="changePasswordModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="changePasswordModalLabel">{{ _('Change password for SSH user') }} {{ current_username }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<!-- Form to change password -->
<form method="post">
<input type="hidden" name="action" value="change_password">
<div class="form-group">
<label for="new_password">{{ _('New Password:') }}</label>
<div class="input-group">
<input type="password" class="form-control" id="new_password" name="new_password" required>
<button type="submit" class="btn btn-primary">{{ _("Change Password") }}</button>
</div>
</div>
<small>{{ _('Password can not be viewed after changing, please make sure to') }} <b>{{ _('copy the new password') }}</b>.</small>
</form>
</div>
</div>
</div></div>
{% endif %}
</section>
<footer class="main-footer btn-toolbar" role="toolbar">
<div class="btn-group" role="group" aria-label="Status">
<label>{{ _('status:') }}</label><b> {% if ssh_status_display == 'ON' %} {{ _('Enabled') }}{% elif ssh_status_display == 'OFF' %} {{ _('Disabled') }}{% else %} {{ _('Unknown') }}{% endif %}</b>
</div>
<div class="ms-auto" role="group" aria-label="Actions">
{% if ssh_status_display == 'ON' %}
<form method="post">
<input type="hidden" name="action" value="disable">
<button type="submit" class="btn btn-primary d-flex align-items-center gap-2">{{ _('Disable SSH Access') }}</button>
</form>
{% elif ssh_status_display == 'OFF' %}
<form method="post">
<input type="hidden" name="action" value="enable">
<button type="submit" class="btn btn-primary d-flex align-items-center gap-2">{{ _('Enable SSH Access') }}</button>
</form>
{% else %}
<form method="post">
<input type="hidden" name="action" value="disable">
<button type="submit" class="btn btn-primary d-flex align-items-center gap-2">{{ _('Disable SSH Access') }}</button>
</form>
<form method="post">
<input type="hidden" name="action" value="enable">
<button type="submit" class="btn btn-primary d-flex align-items-center gap-2">{{ _('Enable SSH Access') }}</button>
</form>
{% endif %}
</div>
</footer>
{% endblock %}

384
templates/admini/ssl.html Normal file
View File

@@ -0,0 +1,384 @@
<!-- ssl.html -->
{% extends 'base.html' %}
{% block content %}
<!-- content for the ssl page -->
{% if domains %}
<style>
@media (min-width: 768px) {
table.table {
table-layout: fixed;
}
table.table td {
word-wrap: break-word;
}
}
@media (max-width: 767px) {
span.advanced-text {display:none;}
}
.hidden {
display: none;
}
.advanced-settings {
align-items: center;
text-align: right;
color: black;
}
.advanced-settings i {
margin right: 5px;
transform: rotate(-90deg);
transition: transform 0.3s ease-in-out;
}
.domain_link {
border-bottom: 1px dashed #999;
text-decoration: none;
color: black;
}
.advanced-settings.active i {
transform: rotate(0);
}
thead {
border: 1px solid rgb(90 86 86 / 11%);
}
th {
color: rgb(0 0 0 / 65%)!important;
background: #fafafa!important;
text-transform: uppercase;
font-weight: 400;
}
</style>
<div class="modal fade" id="viewModal" tabindex="-1" role="dialog" aria-labelledby="viewModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="viewModalLabel">{{ _('View File') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ _('Close') }}"></button>
</div>
<div class="modal-body bg-dark">
<div id="fileContent" class="mb-0"></div>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Search bar -->
<div class="input-group mb-3" style="padding-left:0px;padding-right:0px;">
<input type="text" class="form-control" placeholder="{{ _('Search domains') }}" id="searchDomainInput">
</div>
<script type="module">
const searchDomainInput = document.getElementById("searchDomainInput");
const displaySettingsDiv = document.querySelector(".display_settings");
const domainRows = document.querySelectorAll(".domain_row");
searchDomainInput.addEventListener("input", function () {
const searchTerm = searchDomainInput.value.trim().toLowerCase();
domainRows.forEach(function (row) {
const domainName = row.querySelector("td:nth-child(1)").textContent.toLowerCase();
if (domainName.includes(searchTerm)) {
row.style.display = "table-row";
} else {
row.style.display = "none";
}
});
});
</script>
<!-- Add checkboxes to enable/disable columns -->
<div class="display_settings mb-3" style="display: none;">
<label>{{ _('Display') }}: <input type="checkbox" class="column-toggle" data-column="1" checked> {{ _('Name') }}</label>
<label><input type="checkbox" class="column-toggle" data-column="3" checked> {{ _('DNS Zone') }}</label>
<label><input type="checkbox" class="column-toggle" data-column="4" checked> {{ _('Redirect') }}</label>
</div>
<table class="table" id="ssl_table">
<thead>
<tr>
<th>{{ _('Domain') }}</th>
<th>{{ _('SSL Status') }}</th>
<th>{{ _('Actions') }}</th>
</tr>
</thead>
<tbody>
{% for domain in domains %}
<tr class="domain_row">
<td>{{ domain.domain_url }}</td>
<td style="vertical-align: middle;">
{% if domain.ssl_expiry_date %}
{% if domain.ssl_expiry_date == 'None' %}
<h6><i class="bi bi-x-circle" style="color:red;"></i> {{ _('No SSL installed') }}</h6>
{% else %}
<h6><i class="bi bi-check-circle-fill" style="color:green;" ></i> {{ _('AutoSSL Domain Validated') }}</h6>
{{ _('Expires on') }}: {{ domain.ssl_expiry_date }}<br><small><button class="btn btn-sm btn-transparent view-button" data-file="{{ domain.domain_url }}" data-type="fullchain">{{ _('View Certificate') }}</button> | <button class="btn btn-sm btn-transparent view-button" data-file="{{ domain.domain_url }}" data-type="privkey">{{ _('Private key') }}</button></small>
{% endif %}
{% else %}
<h6><i class="bi bi-exclamation-circle-fill" style="color:orange;"></i> <a href="" id="refresh_dashboard_data" >{{ _('SSL check is in progress.. please click here to refresh.') }}</a></h6>
<script type="module">
function refreshDashboard(event) {
event.preventDefault();
fetch('/dashboard')
.then(response => {
if (response.ok) {
location.reload();
} else {
console.error('Failed to fetch /dashboard:', response.status, response.statusText);
}
})
.catch(error => {
console.error('Error during fetch:', error);
});
}
document.getElementById('refresh_dashboard_data').onclick = refreshDashboard;
</script>
{% endif %}
</td>
<td>
<div class="d-flex gap-2 mt-3 mt-md-0">
{% if domain.ssl_expiry_date != 'None' %}
<button type="button" class="renewBtn btn btn-success d-flex align-items-center gap-2"><i class="bi bi-shield-shaded"></i> {{ _('Renew') }}</button>
<!--button type="button" class="btn btn-white d-flex align-items-center gap-2"><i class="bi bi-shield-slash"></i> {{ _('Exclude') }}</button-->
<form action="/delete_ssl" method="post">
<input type="hidden" name="domain_id" value="{{ domain.domain_id }}">
<input type="hidden" name="domain_name" value="{{ domain.domain_url }}">
<button type="submit" class="btn btn-danger d-flex align-items-center gap-2"><i class="bi bi-trash3"></i> {{ _('Delete') }}</button>
</form>
{% else %}
<form action="/generate_ssl" method="post">
<input type="hidden" name="domain_id" value="{{ domain.domain_id }}">
<input type="hidden" name="domain_name" value="{{ domain.domain_url }}">
<button type="submit" class="btn btn-primary d-flex align-items-center gap-2"><i class="bi bi-shuffle"></i> {{ _('Generate') }}</button>
</form>
<button type="button" class="btn d-flex align-items-center gap-2" data-bs-toggle="modal" data-bs-target="#advancedModal" data-bs-domain="{{ domain.domain_url }}">
<i class="bi bi-gear"></i> {{ _('Custom') }}
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Modal -->
<div class="modal fade" id="advancedModal" tabindex="-1" aria-labelledby="advancedModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="advancedModalLabel">{{ _('Install custom SSL certificate') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form action="/generate_ssl" method="post">
<input type="hidden" class="d-none" name="domain_name" value="">
<div class="d-flex row">
<div class="flex-fill col-6 text-center">
<div class="card">
<div class="card-body">
<svg xmlns="http://www.w3.org/2000/svg" width="5em" height="5em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-certificate"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M15 15m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" /><path d="M13 17.5v4.5l2 -1.5l2 1.5v-4.5" /><path d="M10 19h-5a2 2 0 0 1 -2 -2v-10c0 -1.1 .9 -2 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -1 1.73" /><path d="M6 9l12 0" /><path d="M6 12l3 0" /><path d="M6 15l2 0" /></svg>
<h5 class="card-title">{{ _('Fullchain') }}</h5>
<input type="text" name="fullchain" class="form-control" placeholder="/home/{{current_username}}/path/to/fullchain.pem">
</div>
</div>
</div>
<div class="flex-fill col-6 text-center">
<div class="card">
<div class="card-body">
<svg xmlns="http://www.w3.org/2000/svg" width="5em" height="5em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-key"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M16.555 3.843l3.602 3.602a2.877 2.877 0 0 1 0 4.069l-2.643 2.643a2.877 2.877 0 0 1 -4.069 0l-.301 -.301l-6.558 6.558a2 2 0 0 1 -1.239 .578l-.175 .008h-1.172a1 1 0 0 1 -.993 -.883l-.007 -.117v-1.172a2 2 0 0 1 .467 -1.284l.119 -.13l.414 -.414h2v-2h2v-2l2.144 -2.144l-.301 -.301a2.877 2.877 0 0 1 0 -4.069l2.643 -2.643a2.877 2.877 0 0 1 4.069 0z" /><path d="M15 9h.01" /></svg>
<h5 class="card-title">{{ _('Private Key') }}</h5>
<input type="text" name="key" class="form-control" placeholder="/home/{{current_username}}/path/to/privkey.pem">
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ _('Cancel') }}</button>
<button type="submit" class="btn btn-primary">{{ _('Install Certificate') }}</button>
</form>
</div>
</div>
</div>
</div>
<script>
$('html').on('click', '.view-button', function() {
const button = $(this);
const domain = button.data('file');
const type = button.data('type');
fetch(`/view_ssl_file/${encodeURIComponent(domain)}?filename=${encodeURIComponent(type)}.pem`)
.then(response => response.text())
.then(data => {
const modalTitle = $('#viewModalLabel');
const modalBody = $('#viewModal').find('.modal-body');
modalTitle.text(domain);
modalBody.empty();
const textContent = document.createElement('pre');
textContent.textContent = data;
modalBody.append(textContent);
$('#viewModal').modal('show');
})
.catch(error => {
console.error('{{ _("Error fetching file content:") }}', error);
});
});
$('#renewCertificatesBtn').click(function() {
const toastMessage = `{{ _("Running AutoSSL renewal...") }}`;
const toast = toaster({
body: toastMessage,
className: `border-0 text-white bg-primary`,
});
$.get('/ssl_renew', function(data) {
if (Array.isArray(data) && data.length > 0) {
const errorMessage = data.join('\n');
const toast = toaster({
body: errorMessage,
header: `<div class="d-flex align-items-center"><l-i class="bi bi-lock-fill" class="me-2"></l-i> {{ _("AutoSSL Renewal Completed") }}</div>`,
});
} else {
const toastMessage = JSON.stringify(data, null, 2);
const toast = toaster({
body: toastMessage,
header: `<div class="d-flex align-items-center"><l-i class="bi bi-lock" class="me-2"></l-i> {{ _("AutoSSL Renewal Complete") }}</div>`,
});
}
});
});
$('.renewBtn').click(function () {
var domainUrl = $(this).closest('tr').find('td:first').text();
const toastMessage = `{{ _("Running AutoSSL renewal for ") }}` + domainUrl;
const toast = toaster({
body: toastMessage,
className: `border-0 text-white bg-primary`,
});
$.get('/ssl_renew/' + domainUrl, function (data) {
if (Array.isArray(data) && data.length > 0) {
// Join the error messages into a single string for display
const errorMessage = data.join('\n').replace(/-/g, '');
const toast = toaster({
body: errorMessage,
header: `<div class="d-flex align-items-center"><l-i class="bi bi-lock-fill" class="me-2"></l-i> {{ _("Renewal for") }} ` + domainUrl + ` {{ _("Completed") }}</div>`,
});
} else {
const toastMessage = JSON.stringify(data, null, 2);
const toast = toaster({
body: toastMessage,
header: `<div class="d-flex align-items-center"><l-i class="bi bi-lock-fill" class="me-2"></l-i> {{ _("Renewal for") }} ` + domainUrl + ` {{ _("Completed") }}</div>`,
});
}
});
});
var advancedModal = document.getElementById('advancedModal');
advancedModal.addEventListener('show.bs.modal', function (event) {
var button = event.relatedTarget;
var recipient = button.getAttribute('data-bs-domain');
var modalTitle = advancedModal.querySelector('.modal-title');
var modalBodyInput = advancedModal.querySelector('.modal-body input');
modalTitle.textContent = 'Install custom SSL certificate for ' + recipient;
modalBodyInput.value = recipient;
});
</script>
</section>
<footer class="main-footer btn-toolbar" role="toolbar">
<div class="btn-group" role="group" aria-label="Status">
</div>
<div class="ms-auto" role="group" aria-label="Actions">
<button type="submit" class="btn btn-primary" id="renewCertificatesBtn"><i class="bi bi-plus-lg"></i> {{ _('Run AutoSSL') }}<span class="desktop-only"> {{ _('for All Domains') }}</span></button>
</div>
</div>
</footer>
{% else %}
<!-- Display jumbotron for no domains -->
<div class="jumbotron text-center mt-5" id="jumbotronSection">
<h1>{{ _('No Domains') }}</h1>
<p>{{ _("When you create a domain, the system will attempt to secure that domain with a free Let\'s Encrypt certificate.") }}</p>
<a href="/domains#add-new" class="btn btn-lg btn-primary">
<i class="bi bi-plus-lg"></i> {{ _('Add a Domain Name') }}
</a>
</div>
{% endif %}
<script>
window.onload = function() {
// Autofocus on the input field
document.getElementById('searchDomainInput').focus();
};
</script>
{% endblock %}

View File

@@ -0,0 +1,139 @@
{% extends 'base.html' %}
{% block content %}
<div class="row g-4 py-3 px-3 row-cols-1 row-cols-lg-3">
<!-- First column -->
<div class="col">
<div class="card h-100">
<div class="card-body d-flex align-items-start">
<div class="icon-square text-body-emphasis d-inline-flex align-items-center justify-content-center fs-4 flex-shrink-0 me-3" style="font-size:2em!important;">
<i class="bi bi-clock"></i>
</div>
<div>
<h3 class="fs-2 text-body-emphasis">{{ _('Server Time') }}</h3>
<p>{{ _('Set your servers time zone and synchronize its time with your networks time server.') }}</p>
<a href="/server/timezone" class="btn btn-primary">
{{ _('Change TimeZone') }}
</a>
</div>
</div>
</div>
</div>
<!-- Second column -->
<div class="col">
<div class="card h-100">
<div class="card-body d-flex align-items-start">
<div class="icon-square text-body-emphasis d-inline-flex align-items-center justify-content-center fs-4 flex-shrink-0 me-3" style="font-size:2em!important;">
<i class="bi bi-window-fullscreen"></i>
</div>
<div>
<h3 class="fs-2 text-body-emphasis">{{ _('Service Status') }}</h3>
<p>{{ _('Display a list of OpenPanels monitored services, their current version and statuses.') }}</p>
<a href="/server/services" class="btn btn-primary">
{{ _('Monitor Services') }}
</a>
</div>
</div>
</div>
</div>
<!-- Third column -->
<div class="col">
<div class="card h-100">
<div class="card-body d-flex align-items-start">
<div class="icon-square text-body-emphasis d-inline-flex align-items-center justify-content-center fs-4 flex-shrink-0 me-3" style="font-size:2em!important;">
<i class="bi bi-hdd-network"></i>
</div>
<div>
<h3 class="fs-2 text-body-emphasis">{{service_name}} {{ _('Settings') }}</h3>
<p>{{ _('Configure') }} {{service_name}} {{ _('web server software that handles HTTP requests') }}.</p>
<a href="/server/webserver_conf" class="btn btn-primary">
{{service_name}} {{ _('Configuration') }}
</a>
</div>
</div>
</div>
</div>
<!-- 4th column -->
<div class="col">
<div class="card h-100">
<div class="card-body d-flex align-items-start">
<div class="icon-square text-body-emphasis d-inline-flex align-items-center justify-content-center fs-4 flex-shrink-0 me-3" style="font-size:2em!important;">
<i class="bi bi-database-lock"></i>
</div>
<div>
<h3 class="fs-2 text-body-emphasis">{{ _('MySQL Settings') }}</h3>
<p>{{ _('Make changes to your MySQL® or MariaDB® configuration.') }}</p>
<a href="/server/mysql_conf" class="btn btn-primary">
{{ _('MySQL Configuration') }}
</a>
</div>
</div>
</div>
</div>
<!-- 5th column -->
<div class="col">
<div class="card h-100">
<div class="card-body d-flex align-items-start">
<div class="icon-square text-body-emphasis d-inline-flex align-items-center justify-content-center fs-4 flex-shrink-0 me-3" style="font-size:2em!important;">
<i class="bi bi-code-square"></i>
</div>
<div>
<h3 class="fs-2 text-body-emphasis">{{ _('PHP Settings') }}</h3>
<p>{{ _('Make changes to your PHP configuration.') }}</p>
<a href="/php-version" class="btn btn-primary">
{{ _('PHP Configuration') }}
</a>
</div>
</div>
</div>
</div>
<!-- 6th column -->
<div class="col">
<div class="card h-100">
<div class="card-body d-flex align-items-start">
<div class="icon-square text-body-emphasis d-inline-flex align-items-center justify-content-center fs-4 flex-shrink-0 me-3" style="font-size:2em!important;">
<i class="bi bi-shield-lock"></i>
</div>
<div>
<h3 class="fs-2 text-body-emphasis">{{ _('ModSecurity® Settings') }}</h3>
<p>{{ _('Enable or disable ModSecurity for your domains.') }}</p>
<a href="/server/modsecurity" class="btn btn-primary">
{{ _(' ModSecurity Settings') }}
</a>
</div>
</div>
</div>
</div>
<!-- 7th column -->
<div class="col">
<div class="card h-100">
<div class="card-body d-flex align-items-start">
<div class="icon-square text-body-emphasis d-inline-flex align-items-center justify-content-center fs-4 flex-shrink-0 me-3" style="font-size:2em!important;">
<i class="bi bi-info-square"></i>
</div>
<div>
<h3 class="fs-2 text-body-emphasis">{{ _('Server Information') }}</h3>
<p>{{ _('Displays information about your account and the server.') }}</p>
<a href="/server/info" class="btn btn-primary">
{{ _('Server Information') }}
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,208 @@
<!-- ssl.html -->
{% extends 'base.html' %}
{% block content %}
<!-- content for the ssl page -->
{% if domains %}
<style>
@media (min-width: 768px) {
table.table {
table-layout: fixed;
}
table.table td {
word-wrap: break-word;
}
}
@media (max-width: 767px) {
span.advanced-text {display:none;}
}
.hidden {
display: none;
}
.advanced-settings {
align-items: center;
text-align: right;
color: black;
}
.advanced-settings i {
margin right: 5px;
transform: rotate(-90deg);
transition: transform 0.3s ease-in-out;
}
.domain_link {
border-bottom: 1px dashed #999;
text-decoration: none;
color: black;
}
.advanced-settings.active i {
transform: rotate(0);
}
thead {
border: 1px solid rgb(90 86 86 / 11%);
}
th {
color: rgb(0 0 0 / 65%)!important;
background: #fafafa!important;
text-transform: uppercase;
font-weight: 400;
}
</style>
<div class="row">
<!-- Search bar -->
<div class="input-group mb-3" style="padding-left:0px;padding-right:0px;">
<input type="text" class="form-control" placeholder="{{ _('Search domains') }}" id="searchDomainInput">
</div>
<script>
document.addEventListener("DOMContentLoaded", function() {
// Get references to search input and display settings div
const searchDomainInput = document.getElementById("searchDomainInput");
const displaySettingsDiv = document.querySelector(".display_settings");
// Get all domain rows to be used for filtering
const domainRows = document.querySelectorAll(".domain_row");
// Handle search input changes
searchDomainInput.addEventListener("input", function () {
const searchTerm = searchDomainInput.value.trim().toLowerCase();
// Loop through domain rows and hide/show based on search term
domainRows.forEach(function (row) {
// Move the definition of domainName inside the loop
const domainName = row.querySelector("td:nth-child(1)").textContent.toLowerCase();
// Show row if search term matches domain name or domain URL, otherwise hide it
if (domainName.includes(searchTerm)) {
row.style.display = "table-row";
} else {
row.style.display = "none";
}
});
});
});
</script>
<table class="table" id="ssl_table">
<thead>
<tr>
<th>{{ _('Domain') }}</th>
<th>{{ _('Status') }}</th>
</tr>
</thead>
<tbody>
{% for domain in domains %}
<tr class="domain_row">
<td>{{ domain.domain_url }}</td>
<td>
<div class="d-flex gap-2 mt-3 mt-md-0">
<span id="previous_value_for_domain" style="display:none;">{{ modsecurity_status[domain.domain_url] }}</span>
<form action="/server/modsecurity" method="post">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="modsec_action_{{ domain.domain_url }}" name="modsec_action" {% if modsecurity_status[domain.domain_url] == 'On' %} checked {% endif %}>
</div>
<input type="hidden" name="domain_name" value="{{ domain.domain_url }}">
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<script>
$('html').on('change', 'input[name="modsec_action"]', function() {
const checkbox = $(this);
const domain = checkbox.closest('.domain_row').find('td:first-child').text(); // Get the domain from the first cell of the same row
const newStatus = checkbox.prop('checked') ? 'On' : 'Off';
let btnClass, toastMessage;
if (newStatus === 'On') {
btnClass = 'success';
toastMessage = 'Enabling ModSecurity for ' + domain;
} else if (newStatus === 'Off') {
btnClass = 'danger';
toastMessage = 'Disabling ModSecurity for ' + domain;
}
const toast = toaster({
body: toastMessage,
className: `border-0 text-white bg-${btnClass}`,
});
// Send AJAX request to update ModSecurity status
$.ajax({
type: 'POST',
url: '/server/modsecurity',
data: {
domain_name: domain,
modsec_action: newStatus,
csrfmiddlewaretoken: '{{ csrf_token }}'
},
success: function(data) {
// Handle success if needed
console.log('Status updated successfully');
},
error: function(error) {
console.error('Error updating status:', error);
// Handle error if needed
}
});
});
</script>
</section>
<footer class="main-footer btn-toolbar" role="toolbar">
<div class="btn-group" role="group" aria-label="Status">
</div>
<div class="ms-auto" role="group" aria-label="Actions">
<button type="submit" class="btn btn-primary" id="renewCertificatesBtn">{{ _('Disable ModSecurity') }}<span class="desktop-only"> {{ _('for All Domains') }}</span></button>
</div>
</div>
</footer>
{% else %}
<!-- Display jumbotron for no domains -->
<div class="jumbotron text-center mt-5" id="jumbotronSection">
<h1>{{ _('No Domains') }}</h1>
<a href="/domains#add-new" class="btn btn-lg btn-primary">
<i class="bi bi-plus-lg"></i> {{ _('Add a Domain Name') }}
</a>
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,129 @@
{% extends 'base.html' %}
{% block content %}
<div class="row g-3">
<!-- col -->
<div class="col-xl-12">
<div class="card card-one">
<div class="card-header">
<h6 class="card-title">{{ _('Server Information') }}</h6>
</div><!-- card-header -->
<div class="card-body p-3">
<div class="storage-item">
<div class="flex-fill">
<div class="d-flex justify-content-between mb-1"><span class="fw-medium"><i class="bi bi-rocket"></i> {{ _('Hostname') }}</span>
<span id="node" data-bs-toggle="tooltip" data-bs-placement="left" title="{{ _('Hostname (domain name) that this server is labeled.') }}"><div class="spinner-border" role="status"><span class="visually-hidden">{{ _('Loading...') }}</span></div></span>
</div>
</div><!-- storage-item-body -->
</div>
<hr>
<div class="storage-item">
<div class="flex-fill">
<div class="d-flex justify-content-between mb-1"><span class="fw-medium"><i class="bi bi-bar-chart"></i> {{ _('Average Load') }}</span>
<span id="load" data-bs-toggle="tooltip" data-bs-placement="left" title="{{ _('The system&apos;s average load over the last 1, 5, and 15 minutes') }}"><div class="spinner-border" role="status"><span class="visually-hidden">{{ _('Loading...') }}</span></div></span>
</div>
</div><!-- storage-item-body -->
</div>
<hr>
<div class="storage-item">
<div class="flex-fill">
<div class="d-flex justify-content-between mb-1"><span class="fw-medium"><i class="bi bi-clock-history"></i> {{ _('Uptime') }}</span>
<span id="uptime" data-bs-toggle="tooltip" data-bs-placement="left" title="{{ _('Time the system has been up and running continuously for.') }}"><div class="spinner-border" role="status"><span class="visually-hidden">{{ _('Loading...') }}</span></div></span>
</div>
</div><!-- storage-item-body -->
</div>
<hr>
<div class="storage-item">
<div class="flex-fill">
<div class="d-flex justify-content-between mb-1">
<span class="fw-medium"><i class="bi bi-geo-alt-fill"></i> {{ _('IP Address') }}</span>
<span id="ip" data-bs-toggle="tooltip" data-bs-placement="left" title="{{ _('The system&apos;s IPv4 address.') }}"><div class="spinner-border" role="status"><span class="visually-hidden">{{ _('Loading...') }}</span></div></span>
</div>
</div><!-- storage-item-body -->
</div>
<hr>
<div class="storage-item">
<div class="flex-fill">
<div class="d-flex justify-content-between mb-1">
<span class="fw-medium"><i class="bi bi-window-fullscreen"></i> {{ _('Panel Version') }}</span>
<span class="ff-numerals" data-bs-toggle="tooltip" data-bs-placement="left" title="{{ _('The installed panel software version is')}} {{ panel_version }}">{{ panel_version }}</span>
</div>
</div><!-- storage-item-body -->
</div>
<hr>
<div class="storage-item">
<div class="flex-fill">
<div class="d-flex justify-content-between mb-1">
<span class="fw-medium"><i class="bi bi-ubuntu"></i> {{ _('OS') }}</span>
<span id="system" data-bs-toggle="tooltip" data-bs-placement="left" title="{{ _('Operating System (OS)')}}"><div class="spinner-border" role="status"><span class="visually-hidden">{{ _('Loading...') }}</span></div></span>
</div>
</div><!-- storage-item-body -->
</div>
<hr>
<div class="storage-item">
<div class="flex-fill">
<div class="d-flex justify-content-between mb-1">
<span class="fw-medium"><i class="bi bi-question"></i> {{ _('Release') }}</span>
<span id="release" data-bs-toggle="tooltip" data-bs-placement="left" title="{{ _('OS release') }}"><div class="spinner-border" role="status"><span class="visually-hidden">{{ _('Loading...') }}</span></div></span>
</div>
</div><!-- storage-item-body -->
</div>
<hr><span class="desktop-only">
<div class="storage-item">
<div class="flex-fill">
<div class="d-flex justify-content-between mb-1">
<span class="fw-medium"><i class="bi bi-question"></i> {{ _('Version') }}</span>
<span id="version" data-bs-toggle="tooltip" data-bs-placement="left" title="{{ _('Current OS version') }}"><div class="spinner-border" role="status"><span class="visually-hidden">{{ _('Loading...') }}</span></div></span>
</div>
</div><!-- storage-item-body -->
</div>
<hr></span>
<div class="storage-item">
<div class="flex-fill">
<div class="d-flex justify-content-between mb-1">
<span class="fw-medium"><i class="bi bi-cpu-fill"></i> {{ _('Processor') }}</span>
<span id="processor" data-bs-toggle="tooltip" data-bs-placement="left" title="{{ _('Server Processor Architecture') }}"><div class="spinner-border" role="status"><span class="visually-hidden">{{ _('Loading...') }}</span></div></span> </div>
</div><!-- storage-item-body -->
</div>
</div><!-- card-body -->
</div><!-- card -->
</div><!-- col -->
</div>
<script>
$(document).ready(function() {
$.ajax({
url: '/system/hosting/info',
type: 'GET',
dataType: 'json',
success: function(response) {
// Display each version independently
$('#machine').text(response.machine);
$('#node').text(response.node);
$('#processor').text(response.processor);
$('#release').text(response.release);
$('#system').text(response.system);
$('#version').text(response.version);
$('#ip').text(response.ip);
$('#load').text(response.load_avg);
$('#uptime').text(response.uptime);
},
error: function(error) {
console.log('Error:', error);
}
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,311 @@
{% extends 'base.html' %}
{% block content %}
<style>
td {vertical-align: middle;}
@media (min-width: 768px) {
table.table {
table-layout: fixed;
}
table.table td {
word-wrap: break-word;
}
}
@media (max-width: 767px) {
span.advanced-text {display:none;}
}
.hidden {
display: none;
}
.advanced-settings {
align-items: center;
text-align: right;
color: black;
}
.advanced-settings i {
margin-right: 5px;
transform: rotate(-90deg);
transition: transform 0.3s ease-in-out;
}
[data-bs-theme=light] .domain_link {
color: black;
}
.domain_link {
border-bottom: 1px dashed #999;
text-decoration: none;
}
.advanced-settings.active i {
transform: rotate(0);
}
thead {
border: 1px solid rgb(90 86 86 / 11%);
}
th {
text-transform: uppercase;
font-weight: 400;
}
</style>
<table class="table table-hover">
<thead>
<tr>
<th>{{ _('Service') }}</th>
<th>{{ _('Version') }}</th>
<th>{{ _('Status') }}</th>
</tr>
</thead>
<tbody>
<tr class="domain_row">
<td><img style="width: 30px;" src="/static/images/icons/{{ web_server }}.png"></img> {{ web_server|capitalize }}</td>
<td id="webserver-version"></td>
<td><span class="badge bg-success status-badge" id="webserver-status"></span>&nbsp; <a href="javascript:void(0);" id="restart-web-server" data-toggle="tooltip" data-placement="left" title="{{ _('Restart') }} {{ web_server }}">
<i class="bi bi-arrow-clockwise"></i></a>
</td>
</tr>
<tr class="domain_row">
<td><img style="width: 30px;" src="/static/images/icons/php.svg"></img> <span data-toggle="tooltip" data-placement="top" title="{{ _('Default PHP version is') }} {{php_default_version}}">{{ _('PHP') }}</span></td>
<td id="php-version"></td>
<td><span class="badge bg-success status-badge" id="php-status"></span>
</td>
</tr>
<tr class="domain_row">
<td><img style="width: 30px;" src="/static/images/icons/{% if 'mysql' in mysql_type %}mysql{% elif 'mariadb' in mysql_type %}mariadb{% endif %}.png"></img>{% if mysql_type == 'mysql' %}
{{ _('MySQL') }}
{% elif mysql_type == 'mariadb' %}
{{ _('MariaDB') }}
{% endif %}</span></td>
<td id="mysql-version"></td>
<td><span class="badge bg-success status-badge" id="mysql-status"></span>&nbsp; <a href="javascript:void(0);" id="restart-mysql" data-toggle="tooltip" data-placement="left" title="{{ _('Restart service') }}">
<i class="bi bi-arrow-clockwise"></i></a>
</td>
</tr>
{% if 'phpmyadmin' in enabled_modules %}
<tr class="domain_row">
{% else %}
<tr class="d-none domain_row">
{% endif %}
<td><img style="width: 30px;" src="/static/images/icons/phpmyadmin.png"></img> {{ _('phpMyAdmin') }}</td>
<td id="phpmyadmin-version"></td>
<td><span class="badge bg-success status-badge" id="phpmyadmin-status" data-toggle="tooltip" data-placement="top" title='{{ _("Even when \"Off\" phpMyAdmin starts automatically when you visit /phpmyadmin") }}'></span></td>
</tr>
{% if 'pm2' in enabled_modules %}
<tr class="domain_row">
{% else %}
<tr class="d-none domain_row">
{% endif %}
<td><img style="width: 30px;" src="/static/images/icons/nodejs.png"></img> {{ _('NodeJS') }}</td>
<td id="nodejs-version"></td>
<td><span class="badge bg-success status-badge" id="nodejs-status"></span>
</td>
</tr>
{% if 'pm2' in enabled_modules %}
<tr class="domain_row">
{% else %}
<tr class="d-none domain_row">
{% endif %}
<td><img style="width: 30px;" src="/static/images/icons/python.png"></img> {{ _('Python') }}</td>
<td id="python-version"></td>
<td><span class="badge bg-success status-badge" id="python-status"></span>
</td>
</tr>
{% if 'redis' in enabled_modules %}
<tr class="domain_row">
{% else %}
<tr class="d-none domain_row">
{% endif %}
<td><img style="width: 30px;" src="/static/images/icons/redis.png"></img> {{ _('REDIS') }}</td>
<td id="redis-version"></td>
<td><span class="badge bg-success status-badge" id="redis-status"></span>
</td>
</tr>
{% if 'memcached' in enabled_modules %}
<tr class="domain_row">
{% else %}
<tr class="d-none domain_row">
{% endif %}<td><img style="width: 30px;" src="/static/images/icons/memcached.png"></img> {{ _('Memcached') }}</td>
<td id="memcached-version"></td>
<td><span class="badge bg-success status-badge" id="memcached-status"></span>
</td>
</tr>
{% if 'elasticsearch' in enabled_modules %}
<tr class="domain_row">
{% else %}
<tr class="d-none domain_row">
{% endif %}
<td><img style="width: 30px;" src="/static/images/icons/elasticsearch.png"></img> {{ _('Elasticsearch') }}</td>
<td id="elasticsearch-version"></td>
<td><span class="badge bg-success status-badge" id="elasticsearch-status"></span>
</td>
</tr>
</tbody>
</table>
<script>
function updateStatusInfo() {
$.ajax({
url: '/system/service/status',
type: 'GET',
dataType: 'json',
success: function(response) {
// Define an object to map service names to their corresponding elements
var serviceElements = {
'webserver': $('#webserver-status'),
'memcached': $('#memcached-status'),
'elasticsearch': $('#elasticsearch-status'),
'mysql': $('#mysql-status'),
'phpmyadmin': $('#phpmyadmin-status'),
'nodejs': $('#nodejs-status'),
'php8.2': $('#php-status'),
//'php{{default_php_version}}': $('#php-status'),
'python': $('#python-status'),
'mongodb': $('#mongodb-status'),
'redis': $('#redis-status')
// Add more services and corresponding elements as needed
};
// Iterate through the services and update their status text and badge class
$.each(serviceElements, function(serviceName, element) {
var serviceStatus = response[serviceName];
element.text(serviceStatus);
// Update the badge class based on the service status
if (serviceStatus === 'on') {
element.removeClass('bg-danger').addClass('bg-success');
} else if (serviceStatus === 'off') {
element.removeClass('bg-success').addClass('bg-danger');
}
});
},
error: function(error) {
console.log('Error:', error);
}
});
}
// Initial call to populate version information
updateStatusInfo();
function updateVersionInfo() {
$.ajax({
url: '/system/service/info',
type: 'GET',
dataType: 'json',
success: function(response) {
// Update each version independently
$('#webserver-version').text(response.webserver_version);
$('#memcached-version').text(response.memcached_version);
$('#elasticsearch-version').text(response.elasticsearch_version);
$('#mysql-version').text(response.mysql_version);
$('#phpmyadmin-version').text(response.phpmyadmin_version);
$('#nodejs-version').text(response.nodejs_version);
$('#php-version').text(response.php_version);
$('#python-version').text(response.python_version);
$('#redis-version').text(response.redis_version);
$('#mongodb-version').text(response.mongodb_version);
},
error: function(error) {
console.log('Error:', error);
}
});
}
// Initial call to populate version information
updateVersionInfo();
$('#restart-mysql').on('click', function() {
// Send an AJAX request to restart MySQL
toaster({
body: '{{ _("mysql service restart in progress..") }}',
className: 'border-0 text-white bg-primary',
});
$.ajax({
url: '/system/service/restart/mysql',
type: 'GET',
dataType: 'json',
success: function(response) {
toaster({
body: response.message,
className: 'border-0 text-white bg-success',
});
updateStatusInfo();
},
error: function(error) {
toaster({
body: error,
className: 'border-0 text-white bg-danger',
});
updateStatusInfo();
console.log('Error:', error);
}
});
});
$('#restart-web-server').on('click', function() {
var webServerName = "{{ web_server }}";
toaster({
body: webServerName + ' {{ _("service restart in progress..") }}',
className: 'border-0 text-white bg-primary',
});
$.ajax({
url: '/system/service/restart/' + webServerName,
type: 'GET',
dataType: 'json',
success: function(response) {
toaster({
body: response.message,
className: 'border-0 text-white bg-success',
});
updateStatusInfo();
},
error: function(error) {
toaster({
body: error,
className: 'border-0 text-white bg-danger',
});
updateStatusInfo();
console.log('Error:', error);
}
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,78 @@
{% extends 'base.html' %}
{% block content %}
<!-- system/timezone.html -->
<script type="module">
// Function to attach event listeners
function timezoneattachEventListeners() {
// Select the form and submit button
const form = document.querySelector('form');
const submitButton = document.querySelector('button[type="submit"]');
// Attach the click event listener to the submit button
submitButton.addEventListener('click', async (ev) => {
ev.preventDefault();
const action = submitButton.dataset.action;
const formData = new FormData(form);
const toastMessage = `{{ _('Saving TimeZone...') }}`;
const toast = toaster({
body: toastMessage,
className: `border-0 text-white bg-primary`,
});
try {
const response = await fetch(form.action, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams(formData).toString(),
});
// get the response HTML content
const resultHtml = await response.text();
// Parse the HTML string to extract the content of the specific element
const parser = new DOMParser();
const doc = parser.parseFromString(resultHtml, 'text/html');
const mainScopeContent = doc.getElementById("main-scope")?.innerHTML;
// Replace the content of the element with the ID "main-scope"
const mainScopeElement = document.getElementById("main-scope");
if (mainScopeElement) {
mainScopeElement.innerHTML = mainScopeContent || '';
}
// Reattach event listeners after updating content
timezoneattachEventListeners();
} catch (error) {
console.error('Error:', error);
}
});
}
timezoneattachEventListeners();
</script>
Current timezone: <b>{{ current_timezone_in_docker_container }}</b>
<form method="post" action="{{ url_for('server_timezone_settings') }}" class="mt-3">
<div class="form-group">
<label for="timezone">{{ _('Select Timezone:') }}</label>
<select name="timezone" id="timezone" class="form-control">
{% for timezone in available_timezones %}
<option value="{{ timezone }}" {% if timezone == selected_zone_goes_here %}selected{% endif %}>{{ timezone }}</option>
{% endfor %}
</select>
</div>
<button type="submit" class="btn btn-primary">{{ _('Change Timezone') }}</button>
</form>
{% endblock %}

View File

@@ -0,0 +1,15 @@
{% extends 'base.html' %}
{% block content %}
<p>{{ _('This interface provides command line access to your account on the server.') }}</p>
<p>{{ _('You can open a web terminal for yourself or create a temporary account with a one-time login to share with a third party.') }}</p>
<div class="flex">
<form method="POST" action="/terminal/run">
<a href="/terminal/run" {% if force_domain and force_https %}target="_blank"{% endif %} class="btn btn-primary btn-lg" role="button"><i class="bi bi-terminal"></i> {{ _('Access Web Terminal') }}</a> or <button class="btn btn-outline-primary btn-lg" type="submit"><i class="bi bi-terminal-plus"></i> {{ _('Temporary Share Access') }}</button>
</form>
</div>
{% endblock %}

View File

@@ -0,0 +1,55 @@
{% extends 'base.html' %}
{% block content %}
{% if random_username %}
<h3>{{ _('External Terminal Session created') }}</h3>
<p>{{ _('Temporary session has been created for the web terminal. Login information:') }}</p>
<div class="">
<div class="form-group">
<label for="exampleCombineRow" class="form-label">Logins</label>
<div class="form-field">
<div class="row" id="exampleCombineRow">
<div class="col-6">
<div class="input-group">
<span class="input-group-text">username:</span>
<input type="text" class="form-control" value="{{random_username}}" readonly="">
</div>
</div>
<div class="col-6">
<div class="input-group">
<span class="input-group-text">username:</span>
<input type="text" class="form-control" value="{{random_password}}" readonly="">
</div>
</div>
</div>
</div>
</div>
<div class="form-group">
<label for="readOnlyFieldReal" class="form-label">URL</label>
<div class="form-field">
<input type="text" value="{{ttyd_url}}" class="form-control" readonly="">
</div>
</div>
</div>
{% elif 'exists' in ttyd_url %}
<h3>{{ _('Terminal Session is Already Running') }}</h3>
<p>Currently only one active web terminal session is supported. You need to either wait for the existing user to exit their web terminal session or terminate their session by killing the ttyd process from the <a href="/process-manager">Process Manager</a>.</p>
{% else %}
<p>{{ _('This interface provides command line access to your account on the server.') }}</p>
<iframe width="100%" height="500px" src="{{ ttyd_url }}" frameborder="0" allowfullscreen></iframe>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,279 @@
{% extends 'base.html' %}
{% block content %}
<!-- Specific content for the account.html page -->
<div class="container-xl">
<!-- Account page navigation-->
<nav class="nav nav-line mb-4">
<a class="nav-link active" href="/account" target="">{{ _('Profile') }}</a>
<!--<a class="nav-link" href="#">Preferences</a>-->
{% if 'twofa' in enabled_modules %}
<a class="nav-link" href="/account/2fa">{{ _('2FA') }}</a>
{% endif %}
{% if 'activity' in enabled_modules %}
<a class="nav-link" href="/activity"><span class="desktop-only">{{ _('Account') }} </span>{{ _('Activity') }}</a>
{% endif %}
{% if 'login_history' in enabled_modules %}
<a class="nav-link" href="/account/login-history"><span class="desktop-only">{{ _('Login') }} </span>{{ _('History') }}</a>
{% endif %}
<!--<a class="nav-link" href="#">Email Notifications</a>-->
</nav>
<div class="row">
<div class="col-xl-4">
<!-- Profile picture card-->
<div class="card mb-4 mb-xl-0">
<div class="card-header">{{ _('Profile Picture') }}</div>
<div class="card-body text-center">
<!-- Profile picture image-->
{% if avatar_type == 'gravatar' %}
<img class="img-account-profile rounded-circle mb-2" src="{{ gravatar_image_url }}" alt="Gravatar" style="border-radius: 50%; data-toggle="tooltip" data-placement="bottom" title="{{ _('This image is from Gravatar') }}">
<!-- Profile picture upload button-->
<div class="small font-italic text-muted mb-4">{{ email }}</div>
<a href="https://en.gravatar.com/emails" target="_blank" title="Change Gravatar" class="btn btn-sm btn-primary" type="button">{{ _('Change image on Gravatar') }}</a>
{% elif avatar_type == 'letter' %}
<span class="avatar-initial" style="font-size: xx-large;">{{ current_username[0] }}</span>
{% else %}
<i class="bi bi-person-circle" style="font-size: 128px;"></i>
<div class="small font-italic text-muted mb-4">{{ email }}</div>
{% endif %}
</div>
</div>
</div>
<div class="col-xl-8">
<!-- Account details card-->
<div class="card mb-4">
<div class="card-header">{{ _('Account Details') }}</div>
<div class="card-body">
<form method="POST">
<div class="form-group">
<label class="small mb-1" for="email">{{ _('Email address:') }}</label>
<input type="email" class="form-control" id="email" name="email" placeholder="{{ email }}">
</div>
<div class="form-group">
<label class="small mb-1" for="email">{{ _('Username:') }}</label>
<input type="text" class="form-control" id="username" name="username" placeholder="{{ username }}" disabled>
</div>
<div class="form-group">
<label class="small mb-1" for="password">{{ _('New Password:') }}</label>
<div class="input-group">
<input type="password" class="form-control" id="password" name="password" placeholder="New Password">
<div class="input-group-append">
<button type="button" class="btn btn-white btn-lg" id="show-hide-password">
<i class="bi bi-eye"></i>
</button>
<button type="button" class="btn btn-success btn-lg" id="generate-password">
{{ _('Generate') }}
</button>
</div>
</div>
</div>
<div class="form-group">
<label class="small mb-1" for="confirm_password">{{ _('Confirm New Password:') }}</label>
<div class="input-group">
<input type="password" class="form-control" id="confirm_password" name="confirm_password" placeholder="{{ _('Confirm New Password') }}">
<div class="input-group-append">
<p class="d-none" id="copy-password-link">
<a href="#" style="text-decoration:none;" id="copy-password">
&nbsp; <i class="bi bi-clipboard"> </i> {{ _('Copy Password') }}
</a>
</p>
</div>
</div>
</div>
<br>
<input type="submit" class="btn btn-primary" value="{{ _('Update') }}">
</form>
</div>
</div>
</div>
<div class="col-xl-4">
<!-- dark mode toggle card-->
<div class="card mb-4 mb-xl-0">
<div class="card-header">{{ _('Language') }}</div>
<div class="card-body text-center">
<select class="form-select" id="locale-select" aria-label="Select Language">
<option selected disabled>Choose Language</option>
</select>
<script>
var xhr = new XMLHttpRequest();
xhr.open('GET', '/locales', true);
xhr.onload = function () {
if (xhr.status === 200) {
var response = JSON.parse(xhr.responseText);
var locales = response.locales;
// Update the HTML with the list of locales as options
var localeSelect = document.getElementById('locale-select');
locales.forEach(function (locale) {
var option = document.createElement('option');
option.value = locale;
option.textContent = locale;
localeSelect.appendChild(option);
});
// Add event listener to the select element
localeSelect.addEventListener('change', function () {
var selectedLocale = localeSelect.value;
if (selectedLocale) {
window.location.href = '/change_locale/' + selectedLocale;
}
});
}
};
xhr.send();
</script>
</div>
</div>
<div class="card mb-4 mb-xl-0">
<div class="card-header">{{ _('Theme') }}</div>
<div class="card-body text-center">
<button class="btn js-darkmode-toggle">
<l-i class="bi bi-moon" hidden value="light" style="font-size: large;"></l-i>
<l-i class="bi bi-sun" hidden value="dark" style="color:orange;font-size: large;"></l-i>
</button>
</div>
</div>
</div>
</div>
</div>
<style>
//body{margin-top:20px;
//background-color:#f2f6fc;
//color:#69707a;
//}
.img-account-profile {
height: 10rem;
}
.rounded-circle {
border-radius: 50% !important;
}
.card {
box-shadow: 0 0.15rem 1.75rem 0 rgb(33 40 50 / 15%);
}
.card .card-header {
font-weight: 500;
}
.card-header:first-child {
border-radius: 0.35rem 0.35rem 0 0;
}
.card-header {
padding: 1rem 1.35rem;
margin-bottom: 0;
background-color: rgba(33, 40, 50, 0.03);
border-bottom: 1px solid rgba(33, 40, 50, 0.125);
}
.form-control, .dataTable-input {
display: block;
width: 100%;
padding: 0.875rem 1.125rem;
font-size: 0.875rem;
font-weight: 400;
line-height: 1;
color: #69707a;
background-color: #fff;
background-clip: padding-box;
border: 1px solid #c5ccd6;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
border-radius: 0.35rem;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.nav-borders .nav-link.active {
color: #0061f2;
border-bottom-color: #0061f2;
}
.nav-borders .nav-link {
color: #69707a;
border-bottom-width: 0.125rem;
border-bottom-style: solid;
border-bottom-color: transparent;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
padding-left: 0;
padding-right: 0;
margin-left: 1rem;
margin-right: 1rem;
}
</style>
<script>
// Sve za pass generate, copy to clipboard i hide/show
function togglePasswordVisibility() {
const passwordInput = document.getElementById("password");
const confirmPasswordInput = document.getElementById("confirm_password");
const showHideButton = document.getElementById("show-hide-password");
const eyeIcon = showHideButton.querySelector("i");
if (passwordInput.type === "password") {
passwordInput.type = "text";
confirmPasswordInput.type = "text";
eyeIcon.classList.remove("bi-eye");
eyeIcon.classList.add("bi-eye-slash");
} else {
passwordInput.type = "password";
confirmPasswordInput.type = "password";
eyeIcon.classList.remove("bi-eye-slash");
eyeIcon.classList.add("bi-eye");
}
}
document.getElementById("show-hide-password").addEventListener("click", togglePasswordVisibility);
function generateRandomPassword() {
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}|;:,.<>?";
let password = "";
for (let i = 0; i < 12; i++) {
const randomIndex = Math.floor(Math.random() * characters.length);
password += characters.charAt(randomIndex);
}
return password;
}
function copyToClipboard() {
const passwordInput = document.createElement("input");
passwordInput.type = "text";
passwordInput.value = document.getElementById("password").value;
document.body.appendChild(passwordInput);
passwordInput.select();
document.execCommand("copy");
document.body.removeChild(passwordInput);
const copyLink = document.getElementById("copy-password");
copyLink.innerHTML = '<i class="bi bi-check"></i> {{ _("Copied to clipboard") }}';
setTimeout(() => {
copyLink.innerHTML = '<i class="bi bi-clipboard"></i> {{ _("Copy Password") }}';
}, 3000);
}
document.getElementById("generate-password").addEventListener("click", function() {
const newPassword = generateRandomPassword();
const showHideButton = document.getElementById("show-hide-password");
const eyeIcon = showHideButton.querySelector("i");
document.getElementById("password").type = "text";
document.getElementById("confirm_password").type = "text";
document.getElementById("password").value = newPassword;
document.getElementById("confirm_password").value = newPassword;
eyeIcon.classList.remove("bi-eye");
eyeIcon.classList.add("bi-eye-slash");
document.getElementById("copy-password-link").classList.remove("d-none");
});
document.getElementById("copy-password").addEventListener("click", copyToClipboard);
</script>
{% endblock %}

View File

@@ -0,0 +1,372 @@
<!-- /user/activity.html -->
{% extends 'base.html' %}
{% block content %}
<style>
.activity-group {
margin: 0;
margin-left: 15px;
padding: 0;
list-style: none;
border-left: 2px solid #e2e5ec
}
.activity-group li+li {
margin-top: 30px
}
.activity-date {
padding-left: 40px;
margin-bottom: 10px;
font-size: .75rem;
font-weight: 500;
color: #6e7985;
position: relative
}
.activity-date::before {
content: '';
position: absolute;
top: 7px;
left: 0;
width: 20px;
border-top: 2px solid #e2e5ec
}
.activity-item {
padding-left: 40px;
position: relative
}
.activity-item strong {
font-weight: 600
}
.activity-item::before {
content: '';
position: absolute;
top: -6px;
left: -17px;
width: 32px;
height: 32px;
border-radius: 100%;
background-color: #506fd9;
color: rgba(255,255,255,0.75);
z-index: 5;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-family: 'bootstrap-icons'
}
.activity-item.comment::before {
content: '\F63C';
background-color: #f00d0d
}
.activity-item.post::before {
content: '\F272';
background-color: #1aba1a
}
.activity-item.like::before {
content: '\F5D8';
background-color: #0cb785
}
.activity-item.search::before {
content: '\F0D1';
background-color: #ea4c89
}
.activity-item.logged::before {
content: '\F1BD';
background-color: #228B22
}
.activity-item.loggedout::before {
content: '\F1BE';
background-color: #DC143C
}
.activity-item.folder::before {
content: '\F3D1';
background-color: #ffc107
}
.activity-item.disabled::before {
content: '\F5D7';
background-color: #020107
}
.activity-item .avatar {
flex-shrink: 0
}
</style>
<div class="container-xl">
<!-- Account page navigation-->
<nav class="nav nav-line mb-4">
<a class="nav-link" href="/account" target="">{{ _('Profile') }}</a>
<!--<a class="nav-link" href="#">Preferences</a>-->
{% if 'twofa' in enabled_modules %}
<a class="nav-link" href="/account/2fa">{{ _('2FA') }}</a>
{% endif %}
{% if 'activity' in enabled_modules %}
<a class="nav-link" href="/activity"><span class="desktop-only">{{ _('Account') }} </span>{{ _('Activity') }}</a>
{% endif %}
{% if 'login_history' in enabled_modules %}
<a class="nav-link" href="/account/login-history"><span class="desktop-only">{{ _('Login') }} </span>{{ _('History') }}</a>
{% endif %}
<!--<a class="nav-link" href="#">Email Notifications</a>-->
</nav>
<p class="text-secondary mb-5">{{ _('Activity Log records all important actions performed by user on the panel, such as editing or modifying files, deleting websites, enabling SSH access, etc.') }}</p>
<div class="form-search py-2 mb-4">
<i class="ri-search-line"></i>
<form action="{{ url_for('view_activity_log') }}?show_all=true" style="width:100%;" method="get">
<input type="text" class="form-control" name="search" placeholder="Search activity" value="{{ search_term }}">
</form>
</div><!-- form-search -->
<div class="d-flex align-items-center justify-content-between mb-4">
<h5 class="section-title mb-0">
{% if search_term %}
Showing {{ log_content|length }} of {{ total_lines }} items
{% else %}
Showing {{ items_per_page * (current_page - 1) + 1 }} - {% if items_per_page * current_page > total_lines %}{{ total_lines }}{% else %}{{ items_per_page * current_page }}{% endif %} out of {{ total_lines }} items
{% endif %}
</h5>
{% if search_term %}
{% else %}
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="showAllCheckbox" {% if show_all %}checked{% endif %}>
<label class="form-check-label text-secondary fs-sm" for="showAllCheckbox">
{{ _('Show all activity') }}
</label>
</div>
{% endif %}
</div>
<script>
// Script to handle toggling the "Show all activity" checkbox
const showAllCheckbox = document.getElementById('showAllCheckbox');
showAllCheckbox.addEventListener('change', () => {
const urlSearchParams = new URLSearchParams(window.location.search);
if (showAllCheckbox.checked)
{
urlSearchParams.set('show_all', 'true');
urlSearchParams.delete('page');// Remove the show_all parameter
}
else
{
urlSearchParams.delete('show_all');// Remove the show_all parameter
}
// Update the URL with the new query parameters
window.location.search = urlSearchParams.toString();
});
</script>
<!-- navigacija i na vrhu, bukvalno copypaste sa dna da ne mora da se scrolluje-->
<nav aria-label="Activity Log Navigation">
<ul class="pagination justify-content-center">
{% if search_term or show_all %}
<!-- No pagination controls when searching, uses show_all toggle by default -->
{% else %}
<li class="page-item{% if current_page == 1 %} disabled{% endif %}">
<a class="page-link" href="{{ url_for('view_activity_log', page=1) }}">First</a>
</li>
{% if current_page > 3 %}
<li class="page-item disabled"><a class="page-link" href="#">...</a></li>
{% endif %}
{% if current_page <= 3 %}
{% for p in range(1, total_pages + 1) %}
{% if p == current_page %}
<li class="page-item active"><a class="page-link" href="#">{{ p }}</a></li>
{% elif p <= 5 %}
<li class="page-item"><a class="page-link" href="{{ url_for('view_activity_log', page=p) }}">{{ p }}</a></li>
{% elif p == 6 %}
<li class="page-item disabled"><a class="page-link" href="#">...</a></li>
{% endif %}
{% endfor %}
{% else %}
<li class="page-item">
<a class="page-link" href="{{ url_for('view_activity_log', page=current_page - 2) }}">{{ current_page - 2 }}</a>
</li>
<li class="page-item{% if current_page == current_page - 2 %} active{% endif %}">
<a class="page-link" href="{{ url_for('view_activity_log', page=current_page - 1) }}">{{ current_page - 1 }}</a>
</li>
<li class="page-item active"><a class="page-link" href="#">{{ current_page }}</a></li>
{% for p in range(current_page + 1, current_page + 3) %}
{% if p <= total_pages %}
<li class="page-item"><a class="page-link" href="{{ url_for('view_activity_log', page=p) }}">{{ p }}</a></li>
{% endif %}
{% endfor %}
{% if current_page + 3 < total_pages %}
<li class="page-item disabled"><a class="page-link" href="#">...</a></li>
{% endif %}
{% endif %}
<li class="page-item{% if current_page == total_pages %} disabled{% endif %}">
<a class="page-link" href="{{ url_for('view_activity_log', page=total_pages) }}">{{ _('Last') }}</a>
</li>
{% endif %}
</ul>
</nav>
<ul class="activity-group mb-5">
{% for log_entry in log_content %}
{% set parts = log_entry.split(' ') %}
{% if parts|length >= 6 %}
{% set timestamp = parts[0] ~ ' ' ~ parts[1] ~ ' ' ~ parts[2] %}
{% set ip = parts[3] %}
{% set admin = parts[4] %}
{% if "Administrator" in admin %}
{% set action_parts = parts[5:] %}
{% set action = action_parts|join(' ') %}
{% else %}
{% set user = parts[5] %}
{% set action_parts = parts[6:] %}
{% set action = action_parts|join(' ') %}
{% endif %}
{% set action_classes = "activity-item" %}
{% if "accessed" in action %}
{% set action_classes = action_classes ~ " search" %}
{% elif "deleted" in action or "edited MySQL configuration" in action %}
{% set action_classes = action_classes ~ " comment" %}
{% elif "File Manager" in action %}
{% set action_classes = action_classes ~ " folder" %}
{% elif "enabled" in action %}
{% set action_classes = action_classes ~ " like" %}
{% elif "disabled" in action %}
{% set action_classes = action_classes ~ " disabled" %}
{% elif "logged in" in action %}
{% set action_classes = action_classes ~ " logged" %}
{% elif "logged out" in action %}
{% set action_classes = action_classes ~ " loggedout" %}
{% elif "wordpress" in action %}
{% set action_classes = action_classes ~ " wordpress" %}
{% else %}
{% set action_classes = action_classes ~ " post" %}
{% endif %}
{% set show_div = "/home/" ~ user in action or "created a" in action or "changed" in action or "edited MySQL configuration" in action %}
<li class="activity-date">{{ timestamp }}</li>
<li class="{{ action_classes }}">
{% if "Administrator" in admin %}
<p class="d-sm-flex align-items-center mb-2">
<div class="card card-comment bg-primary">
<div class="card-body">{% if "Administrator" in admin %}{{admin}}{% else%}{{ user }}{% endif %} <strong>{{ action }}</strong></div>
</div>
</p>
{% else %}
{% if show_div %}
<p class="d-sm-flex align-items-center mb-2">
{% if gravatar_image_url is not none %}
<a href="" class="avatar avatar-thumb me-2 d-none d-sm-inline"><img src="{{ gravatar_image_url }}" alt=""></a>
{% endif %}
<span class="fs-sm">{{ user }}</span>
<span class="fs-xs text-secondary ms-auto"><a href="/account/login-history">{{ ip }}</a></span>
</p>
<div class="card card-comment">
<div class="card-body">{% if "Administrator" in admin %}{{admin}}{% else%}{{ user }}{% endif %} <strong>{{ action }}</strong></div>
</div>
{% else %}
<p class="d-sm-flex align-items-center mb-0">
{% if gravatar_image_url is not none %}
<a href="" class="avatar avatar-thumb me-2 d-none d-sm-inline"><img src="{{ gravatar_image_url }}" alt=""></a>
{% endif %}
<span class="fs-sm">{% if "Administrator" in admin %}{{admin}}{% else%}{{ user }}{% endif %} <strong>{{ action }}</strong></span>
<span class="fs-xs text-secondary ms-auto"><a href="/account/login-history">{{ ip }}</a></span>
</p>
{% endif %}
{% endif %}
</li>
{% endif %}
{% endfor %}
</ul>
<nav aria-label="Activity Log Navigation">
<ul class="pagination justify-content-center">
{% if search_term or show_all %}
<!-- No pagination controls when searching, uses show_all toggle by default -->
{% else %}
<li class="page-item{% if current_page == 1 %} disabled{% endif %}">
<a class="page-link" href="{{ url_for('view_activity_log', page=1) }}">First</a>
</li>
{% if current_page > 3 %}
<li class="page-item disabled"><a class="page-link" href="#">...</a></li>
{% endif %}
{% if current_page <= 3 %}
{% for p in range(1, total_pages + 1) %}
{% if p == current_page %}
<li class="page-item active"><a class="page-link" href="#">{{ p }}</a></li>
{% elif p <= 5 %}
<li class="page-item"><a class="page-link" href="{{ url_for('view_activity_log', page=p) }}">{{ p }}</a></li>
{% elif p == 6 %}
<li class="page-item disabled"><a class="page-link" href="#">...</a></li>
{% endif %}
{% endfor %}
{% else %}
<li class="page-item">
<a class="page-link" href="{{ url_for('view_activity_log', page=current_page - 2) }}">{{ current_page - 2 }}</a>
</li>
<li class="page-item{% if current_page == current_page - 2 %} active{% endif %}">
<a class="page-link" href="{{ url_for('view_activity_log', page=current_page - 1) }}">{{ current_page - 1 }}</a>
</li>
<li class="page-item active"><a class="page-link" href="#">{{ current_page }}</a></li>
{% for p in range(current_page + 1, current_page + 3) %}
{% if p <= total_pages %}
<li class="page-item"><a class="page-link" href="{{ url_for('view_activity_log', page=p) }}">{{ p }}</a></li>
{% endif %}
{% endfor %}
{% if current_page + 3 < total_pages %}
<li class="page-item disabled"><a class="page-link" href="#">...</a></li>
{% endif %}
{% endif %}
<li class="page-item{% if current_page == total_pages %} disabled{% endif %}">
<a class="page-link" href="{{ url_for('view_activity_log', page=total_pages) }}">Last</a>
</li>
{% endif %}
</ul>
</nav>
</div>
{% endblock %}

View File

@@ -0,0 +1,101 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Meta -->
<meta name="description" content="">
<meta name="author" content="UNLIMITED.RS">
<!-- Favicon -->
<link rel="shortcut icon" type="image/x-icon" href="/static/assets/img/favicon.png">
<title>{% if brand_name %}{{brand_name}}{% else %}{{brand}}{% endif %}</title>
<!-- Template CSS -->
<link rel="stylesheet" href="/static/assets/css/style.min.css">
</head>
<body class="page-sign">
<div class="card card-sign">
<div class="card-header">
<a href="." class="header-logo mb-4">{% if brand_name %}{{brand_name}}{% else %}openpanel{% endif %}</a>
{% if email_sent %}
<div class="text-center" style="font-size: 10em;color:green;" role="img" aria-label="Check circle - large preview">
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" fill="currentColor" class="bi bi-check-circle" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"></path>
<path d="M10.97 4.97a.235.235 0 0 0-.02.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05z"></path>
</svg>
</div>
<h3 class="card-title text-center">Email Sent</h3>
<p class="card-text">A password reset email has been sent to your inbox, and it will remain valid for the next 30 minutes. Kindly review your email to proceed with the password reset.</p>
</div><!-- card-header -->
<div class="card-body">
<label class="form-label d-flex mt-3 justify-content-between"><a href="/login" tabindex="-1">← Back to login
</a></label>
</div>
</div><!-- card -->
{% elif not email_sent %}
{% if not twofa_enabled %}
<h3 class="card-title">Reset Password</h3>
<p class="card-text">Please provide the email address associated with your username, and a link to reset your password will be emailed to you.</p>
{% elif twofa_enabled %}
<h3 class="card-title">Verification code (2FA)</h3>
<p class="card-text">TwoFactor Authentification is enabled, please enter the 2FA code from your device.</p>
{% endif %}
</div><!-- card-header -->
<div class="card-body">
<form method="POST">
{% if not twofa_enabled %}
<!-- Username and password fields only if 2FA is not enabled -->
<div class="mb-4">
<label class="form-label">Email</label>
<input type="email" id="email" name="email" required class="form-control" placeholder="Enter your email">
</div>
{% endif %}
{% if twofa_enabled %}
<!-- 2FA field if 2FA is enabled -->
<input type="hidden" name="user_id" value="{{ user_id }}">
<div class="mb-4">
<label class="form-label">Two-Factor Authentication Code</label>
<input type="password" autofocus id="twofa_code" name="twofa_code" class="form-control" required placeholder="Enter your 2FA code">
</div>
{% endif %}
<!-- Display error messages -->
{% if error_message %}
<div class="mb-3 text-danger">
{{ error_message }}
</div>
{% endif %}
<button class="btn btn-primary btn-sign" type="submit">Reset Password</button>
<label class="form-label d-flex mt-3 justify-content-between"><a href="/login" tabindex="-1">← Back to login
</a></label>
</form>
</div><!-- card-body -->
</div><!-- card -->
{% endif %}
<script src="/static/lib/jquery/jquery.min.js"></script>
<script src="/static/lib/bootstrap/js/bootstrap.bundle.min.js"></script>
<script>
'use script'
var skinMode = localStorage.getItem('skin-mode');
if(skinMode) {
$('html').attr('data-skin', 'dark');
}
</script>
</body>
</html>

View File

@@ -0,0 +1,250 @@
<!-- history_usage.html -->
{% extends 'base.html' %}
{% block content %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
<div class="row">
{% if charts_mode == 'one' %}
<div class="">
<div class="col-md-12">
<div class="card mb-4">
<div class="card-header"><h6>{{ _('Historical CPU and Memory Usage:') }}</h6></div>
<div class="card-body">
<canvas id="combinedChart"></canvas>
</div>
</div>
</div>
</div>
{% elif charts_mode == 'two' %}
<div class="row" style="padding: 0px!important; margin: 0px!important;">
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header"><h6>{{ _('Historical CPU Usage:') }}</h6></div>
<div class="card-body">
<canvas id="cpuChart"></canvas>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header"><h6>{{ _('Historical Memory Usage:') }}</h6></div>
<div class="card-body">
<canvas id="ramChart"></canvas>
</div>
</div>
</div>
</div>
{% else %}
{% endif %}
<div class="d-flex align-items-center justify-content-between mb-4">
<h5 class="section-title mb-0">
{% if show_all %}
Showing 1 - {{ total_lines }} of {{ total_lines }} items
{% else %}
Showing {{ items_per_page * (current_page - 1) + 1 }} - {% if items_per_page * current_page > total_lines %}{{ total_lines }}{% else %}{{ items_per_page * current_page }}{% endif %} out of {{ total_lines }} items
{% endif %}
</h5>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="showAllCheckboxOnHistoryPage" {% if show_all %}checked{% endif %}>
<label class="form-check-label text-secondary fs-sm" for="showAllCheckboxOnHistoryPage">
{{ _('Show all data') }}
</label>
</div>
</div>
<div class="container">
<table class="table" id="usage">
<thead>
<tr>
<th><i class="bi bi-calendar-month"></i> {{ _("Date") }}</th>
<th><i class="bi bi-cpu"> </i>{{ _("CPU &#37;") }}</th>
<th><i class="bi bi-memory"></i> {{ _("Memory &#37;") }}</th>
<th><i class="bi bi-ethernet"></i> {{ _("Net I/O") }}</th>
<th><i class="bi bi-device-ssd"></i> {{ _("Block I/O") }}</th>
</tr>
</thead>
<tbody>
{% for entry in usage_data %}
<tr>
<td>{{ entry.timestamp }}</td>
<td>{{ entry.cpu_percent | round(2) }}</td>
<td>{{ entry.mem_percent | round(2) }}</td>
<td>{{ entry.net_io }}</td>
<td>{{ entry.block_io }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<nav aria-label="Usage history Navigation">
<ul class="pagination justify-content-center">
{% if show_all %}
<!-- No pagination controls when searching, uses show_all toggle by default -->
{% else %}
<li class="page-item{% if current_page == 1 %} disabled{% endif %}">
<a class="page-link" href="{{ url_for('usage_history', page=1) }}">First</a>
</li>
{% if current_page > 3 %}
<li class="page-item disabled"><a class="page-link" href="#">...</a></li>
{% endif %}
{% if current_page <= 3 %}
{% for p in range(1, total_pages + 1) %}
{% if p == current_page %}
<li class="page-item active"><a class="page-link" href="#">{{ p }}</a></li>
{% elif p <= 5 %}
<li class="page-item"><a class="page-link" href="{{ url_for('usage_history', page=p) }}">{{ p }}</a></li>
{% elif p == 6 %}
<li class="page-item disabled"><a class="page-link" href="#">...</a></li>
{% endif %}
{% endfor %}
{% else %}
<li class="page-item">
<a class="page-link" href="{{ url_for('usage_history', page=current_page - 2) }}">{{ current_page - 2 }}</a>
</li>
<li class="page-item{% if current_page == current_page - 2 %} active{% endif %}">
<a class="page-link" href="{{ url_for('usage_history', page=current_page - 1) }}">{{ current_page - 1 }}</a>
</li>
<li class="page-item active"><a class="page-link" href="#">{{ current_page }}</a></li>
{% for p in range(current_page + 1, current_page + 3) %}
{% if p <= total_pages %}
<li class="page-item"><a class="page-link" href="{{ url_for('usage_history', page=p) }}">{{ p }}</a></li>
{% endif %}
{% endfor %}
{% if current_page + 3 < total_pages %}
<li class="page-item disabled"><a class="page-link" href="#">...</a></li>
{% endif %}
{% endif %}
<li class="page-item{% if current_page == total_pages %} disabled{% endif %}">
<a class="page-link" href="{{ url_for('usage_history', page=total_pages) }}">Last</a>
</li>
{% endif %}
</ul>
</nav>
<script>
// Script to handle toggling the "Show all activity" checkbox
const showAllCheckboxOnHistoryPage = document.getElementById('showAllCheckboxOnHistoryPage');
showAllCheckboxOnHistoryPage.addEventListener('change', () => {
const urlSearchParams = new URLSearchParams(window.location.search);
if (showAllCheckboxOnHistoryPage.checked) {
urlSearchParams.set('show_all', 'true');
urlSearchParams.delete('page');// Remove the show_all parameter
}
else {
urlSearchParams.delete('show_all');// Remove the show_all parameter
}
// Update the URL with the new query parameters
window.location.search = urlSearchParams.toString();
});
// Get the table rows and initialize arrays for data
const usageTable = document.getElementById('usage');
const tableRows = usageTable.querySelectorAll('tbody tr');
const timestamps = [];
const cpuData = [];
const ramData = [];
// Extract data from table rows
tableRows.forEach(row => {
const cells = row.querySelectorAll('td');
timestamps.push(cells[0].textContent);
cpuData.push(parseFloat(cells[1].textContent));
ramData.push(parseFloat(cells[2].textContent));
});
// Reverse the arrays
timestamps.reverse();
cpuData.reverse();
ramData.reverse();
</script>
{% if charts_mode == 'one' %}
<script>
// Create combined chart
const combinedChartCtx = document.getElementById('combinedChart').getContext('2d');
new Chart(combinedChartCtx, {
type: 'line',
data: {
labels: timestamps,
datasets: [
{
label: 'CPU %',
data: cpuData,
borderColor: 'rgba(75, 192, 192, 1)',
fill: false,
},
{
label: 'Memory %',
data: ramData,
borderColor: 'rgba(255, 99, 132, 1)',
fill: false,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
},
});
</script>
{% elif charts_mode == 'two' %}
<script>
// Create CPU chart
const cpuChartCtx = document.getElementById('cpuChart').getContext('2d');
new Chart(cpuChartCtx, {
type: 'line',
data: {
labels: timestamps,
datasets: [{
label: 'CPU %',
data: cpuData,
borderColor: 'rgba(75, 192, 192, 1)',
fill: false
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
}
});
// Create RAM chart
const ramChartCtx = document.getElementById('ramChart').getContext('2d');
new Chart(ramChartCtx, {
type: 'line',
data: {
labels: timestamps,
datasets: [{
label: 'Memory %',
data: ramData,
borderColor: 'rgba(255, 99, 132, 1)',
fill: false
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
}
});
</script>
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,189 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="Stefan Pejcic">
<meta name="robots" content="noindex">
<link rel="shortcut icon" type="image/x-icon" href="/static/assets/img/favicon.svg">
<title>{% if brand_name %}{{brand_name}}{% else %}OpenPanel{% endif %}</title>
<style>
body {
background-image: url("{{ url_for('static', filename='images/bg-body.png') }}");
background-repeat: no-repeat;
background-position: left; }
}
*,
*:before,
*:after{
padding: 0;
margin: 0;
box-sizing: border-box;
}
#eye-wrapper{
background-color: white;
height: 28px;
width: 28px;
border-radius: 50%;
position: absolute;
transform: translate(0,-50%);
top: 50%;
right: 15px;
transition: 0.5s;
cursor: pointer;
z-index: 1;
}
#open,#close{
position: absolute;
margin: auto;
left: 0;
right: 0;
top: 0.5px;
width: 25px;
}
#open{
display: none;
}
</style>
<!-- Template CSS -->
<link rel="stylesheet" href="/static/css3/admini.min.css">
</head>
<body class="mx-auto mwp-480 d-xl-flex align-items-center justify-content-center">
<div class="card card-sign">
<div class="card-header bg-dark text-center" style="display:inline;">
<h3>{{ _('Sign In') }}</h3>
<p class="card-text">{{ _('Welcome back! Please sign in to continue.') }}</p>
</div><!-- card-header -->
<div class="card-body">
<form method="POST" action="{{ url_for('login') }}" onsubmit="convertToLowerCase()">
{% if not twofa_enabled %}
<!-- Username and password fields only if 2FA is not enabled -->
<div class="mb-4">
<label class="form-label">{{ _('Username') }}</label>
<div class="input-group">
<span class="input-group-text">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-person-fill" viewBox="0 0 16 16">
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6"/>
</svg>
</span>
<input type="text" id="username" name="username" required class="form-control" placeholder="{{ _('Enter your panel username') }}" {% if 'Unrecognized' in error_message %} autofocus value="{{username}}"{% elif username %}value="{{username}}"{% else %}autofocus{% endif %}>
</div>
</div>
<div class="mb-4">
<label class="form-label d-flex justify-content-between">{{ _('Password') }}{% if password_reset == 'yes' %} <a href="/reset_password" tabindex="-1">{{ _('Forgot password?') }}</a>{% endif %}</label>
<div class="input-group">
<span class="input-group-text">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-shield-lock-fill" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 0c-.69 0-1.843.265-2.928.56-1.11.3-2.229.655-2.887.87a1.54 1.54 0 0 0-1.044 1.262c-.596 4.477.787 7.795 2.465 9.99a11.8 11.8 0 0 0 2.517 2.453c.386.273.744.482 1.048.625.28.132.581.24.829.24s.548-.108.829-.24a7 7 0 0 0 1.048-.625 11.8 11.8 0 0 0 2.517-2.453c1.678-2.195 3.061-5.513 2.465-9.99a1.54 1.54 0 0 0-1.044-1.263 63 63 0 0 0-2.887-.87C9.843.266 8.69 0 8 0m0 5a1.5 1.5 0 0 1 .5 2.915l.385 1.99a.5.5 0 0 1-.491.595h-.788a.5.5 0 0 1-.49-.595l.384-1.99A1.5 1.5 0 0 1 8 5"/>
</svg>
</span>
<input type="password" id="password" name="password" class="form-control" required placeholder="{{ _('Enter your password') }}" {% if username and 'password' in error_message %}autofocus{% endif %}>
<div id="eye-wrapper" onclick="toggle()">
<img src="{{ url_for('static', filename='image/login/eye-open-01-01.svg') }}" id="open">
<img src="{{ url_for('static', filename='image/login/eye-close-01-01.svg') }}" id="close">
</div>
</div>
</div>
{% endif %}
{% if twofa_enabled %}
<!-- 2FA field if 2FA is enabled -->
<input type="hidden" name="user_id" value="{{ user_id }}">
<div class="mb-4">
<label class="form-label">{{ _('Two-Factor Authentication Code') }}</label>
<div class="input-group">
<span class="input-group-text">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-phone" viewBox="0 0 16 16">
<path d="M11 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1zM5 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2z"/>
<path d="M8 14a1 1 0 1 0 0-2 1 1 0 0 0 0 2"/>
</svg>
</span>
<input type="password" autofocus id="twofa_code" name="twofa_code" class="form-control" required placeholder="{{ _('Enter your 2FA code') }}">
</div>
</div>
{% endif %}
<button class="btn btn-primary btn-sign w-100" type="submit">{{ _('Sign In') }}</button>
</form>
<script>
var state = false;
function toggle(){
if(state){
document.getElementById("password").setAttribute("type","password");
document.getElementById("open").style.display= 'none';
document.getElementById("close").style.display= 'block';
state = false;
}
else{
document.getElementById("password").setAttribute("type","text");
document.getElementById("open").style.display= 'block';
document.getElementById("close").style.display= 'none';
state = true;
}
}
function convertToLowerCase() {
var usernameInput = document.getElementById('username');
usernameInput.value = usernameInput.value.toLowerCase();
}
</script>
</div><!-- card-body -->
<!-- Display error messages -->
{% if error_message %}
<div class="card-footer mb-0 text-danger">
{{ error_message }}
</div>
{% endif %}
</div>
<div class="d-xl-flex align-items-center justify-content-center" style="position:absolute; bottom:0;">
{% if brand_name %}<a href="." class="mb-4" style="text-decoration: none;"><p class="lead"><b>{{brand_name}}{% else %}<a href="https://openpanel.com?utm=2083_login_page" target="_blank" class="mb-4" style="text-decoration: none;"><p class="lead"><b><svg version="1.0" style="vertical-align:middle;" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 213.000000 215.000000" preserveAspectRatio="xMidYMid meet"><g transform="translate(0.000000,215.000000) scale(0.100000,-0.100000)" fill="currentColor" stroke="none"><path d="M990 2071 c-39 -13 -141 -66 -248 -129 -53 -32 -176 -103 -272 -158 -206 -117 -276 -177 -306 -264 -17 -50 -19 -88 -19 -460 0 -476 0 -474 94 -568 55 -56 124 -98 604 -369 169 -95 256 -104 384 -37 104 54 532 303 608 353 76 50 126 113 147 184 8 30 12 160 12 447 0 395 -1 406 -22 461 -34 85 -98 138 -317 264 -104 59 -237 136 -295 170 -153 90 -194 107 -275 111 -38 2 -81 0 -95 -5z m205 -561 c66 -38 166 -95 223 -127 l102 -58 0 -262 c0 -262 0 -263 -22 -276 -13 -8 -52 -31 -88 -51 -36 -21 -126 -72 -200 -115 l-135 -78 -3 261 -3 261 -166 95 c-91 52 -190 109 -219 125 -30 17 -52 34 -51 39 3 9 424 256 437 255 3 0 59 -31 125 -69z"></path></g></svg> OpenPanel{% endif %}</b></p></a>
</div>
<script src="/static/lib/jquery/jquery.min.js"></script>
<script src="/static/lib/bootstrap/js/bootstrap.bundle.min.js"></script>
<script>
'use script'
var skinMode = localStorage.getItem('skin-mode');
if(skinMode) {
$('html').attr('data-skin', 'dark');
}
</script>
</body>
</html>

View File

@@ -0,0 +1,105 @@
<!-- loginlog.html -->
{% extends 'base.html' %}
{% block content %}
<div class="container-xl">
<!-- Account page navigation-->
<nav class="nav nav-line mb-4">
<a class="nav-link" href="/account" target="">{{ _('Profile') }}</a>
<!--<a class="nav-link" href="#">Preferences</a>-->
{% if 'twofa' in enabled_modules %}
<a class="nav-link" href="/account/2fa">{{ _('2FA') }}</a>
{% endif %}
{% if 'activity' in enabled_modules %}
<a class="nav-link" href="/activity"><span class="desktop-only">{{ _('Account') }} </span>{{ _('Activity') }}</a>
{% endif %}
<a class="nav-link active" href="/account/login-history"><span class="desktop-only">{{ _('Login') }} </span>{{ _('History') }}</a>
<!--<a class="nav-link" href="#">Email Notifications</a>-->
</nav>
<!-- Payment methods card-->
<div class="card card-header-actions mb-4">
<div class="card-header">
Successfull logins
</div>
<div class="card-body px-0">
<!-- Payment method 1-->
{% for login_record in last_login_data %}
<div class="d-flex align-items-center justify-content-between px-4">
<div class="d-flex align-items-center">
{% with country_code_lower=login_record.country_code|lower %}
<img src="/static/flags/{{ country_code_lower }}.png" alt="{{ login_record.country_code }}"></img>
<span class="desktop-only"> &nbsp; {{ login_record.country_code }}</span>
{% endwith %}
<div class="ms-4">
<div class="small"> {{ login_record.ip }}
<span class="desktop-only"><i class="bi bi-clipboard copy-ip-icon" data-ip="{{ login_record.ip }}" title="{{ _('Click to copy IP') }}" onclick="copyToClipboard(this)"></i></span></div>
<div class="text-xs text-muted tooltip">{{ _('Copied!') }}</div>
</div>
</div>
<div class="ms-4">
<div class="d-inline-flex p-2 border">{{ login_record.login_time }}</div>
</div>
</div>
<hr>
{% endfor %}
</div>
</div>
<script>
function copyToClipboard(icon) {
const originalIconClass = icon.className;
const ipToCopy = icon.getAttribute('data-ip');
const textarea = document.createElement('textarea');
textarea.value = ipToCopy;
document.body.appendChild(textarea);
textarea.select();
textarea.setSelectionRange(0, 99999);
document.execCommand('copy');
document.body.removeChild(textarea);
icon.className = 'bi bi-clipboard-check-fill';
const tooltip = icon.nextElementSibling;
tooltip.style.visibility = 'visible';
tooltip.style.opacity = '1';
setTimeout(() => {
tooltip.style.visibility = 'hidden';
tooltip.style.opacity = '0';
icon.className = originalIconClass;
}, 3000);
}
</script>
<script>
//sortiraj od najnovijeg logina
function reverseTableRows() {
const table = document.querySelector(".table tbody");
const rows = table.querySelectorAll("tr");
const reversedRows = Array.from(rows).reverse();
table.innerHTML = '';
reversedRows.forEach(row => table.appendChild(row));
}
window.addEventListener("load", reverseTableRows);
</script>
<style>
.tooltip {
visibility: hidden;
position: absolute;
top: 50%;
left: 172px;
transform: translate(-50%, -50%);
padding: 5px;
background-color: #000;
color: #fff;
border-radius: 5px;
opacity: 0;
transition: opacity 0.3s ease;
}
.position-relative {
position: relative;
}
</style>
{% endblock %}

View File

@@ -0,0 +1,28 @@
{% extends 'base.html' %}
{% block content %}
<div class="container-xl">
<div class="row">
<form method="POST" action="{{ url_for('account_notifications') }}">
<h4>{{ title }}</h4>
<div>
<p>Receive email notifications for:</p>
{% for key, value in notifications.items() %}
<div>
<div class="form-check form-switch">
<input class="form-check-input" name="{{ key }}" id="{{ key }}" type="checkbox" role="switch" value="1" {% if value == '1' %}checked{% endif %}>
<label class="form-check-label" for="{{ key }}">{{ key[6:].replace('_', ' ') if key.startswith('notify') else key.replace('_', ' ').title() }} </label>
</div>
</div>
{% endfor %}
</div>
<button class="btn btn-primary" type="submit">Save Preferences</button>
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,184 @@
{% extends 'base.html' %}
{% block content %}
<div class="row">
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header"><h6><i class="bi bi-cpu"></i> {{ _("CPU") }}</h6></div>
<div class="card-body text-center">
<div id="cpuGauge"></div>
<p>{{ _('Current CPU usage:') }} {{ container_stats['CPU %'] }}</br>&nbsp;</p>
<a id="topCpuButton" class="btn btn-primary btn-sm mb-3" data-bs-toggle="collapse" href="#show_top_cpu_processes" role="button" aria-expanded="false" aria-controls="show_top_cpu_processes">{{ _('View top processes') }} </a> <a href="/usage/history" class="btn btn-dark btn-sm mb-3" type="button">{{ _("View past usage") }}</a>
<div class="collapse" id="show_top_cpu_processes">
<div id="cpu_current"></div>
</div>
</div></div></div>
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header"><h6><i class="bi bi-memory"></i> {{ _("RAM") }}</h6></div>
<div class="card-body text-center">
<div id="ramGauge"></div>
<p>{{ _('Current Memory usage:') }} {{ container_stats['Memory %'] }}<br>({{ container_stats['Memory Usage'] }} / {{ container_stats['Memory Limit'] }})</p>
<a id="topMemButton" class="btn btn-primary btn-sm mb-3" data-bs-toggle="collapse" href="#show_top_mem_processes" role="button" aria-expanded="false" aria-controls="show_top_mem_processes">{{ _('View top processes') }} </a> <a href="/usage/history" class="btn btn-dark btn-sm mb-3" type="button">{{ _("View past usage") }}</a>
<div class="collapse" id="show_top_mem_processes">
<div id="ram_current"></div>
</div>
</div>
</div>
</div>
<script>
// Function to send AJAX request
function sendAjaxRequest(sortParameter, targetDiv) {
// Create a new XMLHttpRequest object
var xhr = new XMLHttpRequest();
// Specify the request method and URL
var url = "/usage/current?sort_parameter=" + sortParameter;
xhr.open("GET", url, true);
// Set up the callback function to handle the response
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
// Parse the JSON response
var response = JSON.parse(xhr.responseText);
// Update the content of the target div with a formatted table
document.getElementById(targetDiv).innerHTML = formatTable(response.result, sortParameter);
}
};
// Send the request
xhr.send();
}
// Function to format data as a Bootstrap table
function formatTable(data, sortParameter) {
var columns;
if (sortParameter === "cpu") {
columns = ["PID", "%CPU", "TIME", "COMMAND"];
} else if (sortParameter === "mem") {
columns = ["PID", "%MEM", "TIME", "COMMAND"];
} else {
// Default columns
columns = ["PID", "Column3", "Column4", "Column5"];
}
var table = "<table class='table table-bordered table-hover'>" +
"<thead><tr>";
// Add the specified columns to the table header
for (var i = 0; i < columns.length; i++) {
table += "<th>" + columns[i] + "</th>";
}
table += "</tr></thead>" +
"<tbody>";
// Iterate through each row and create table rows
for (var i = 0; i < data.length; i++) {
table += "<tr>";
// Add the specified columns to the table rows
for (var j = 0; j < columns.length; j++) {
table += "<td>" + data[i][columns[j]] + "</td>";
}
table += "</tr>";
}
table += "</tbody></table>";
return table;
}
// Attach click event listeners to the buttons
document.getElementById("topCpuButton").addEventListener("click", function () {
sendAjaxRequest("cpu", "cpu_current");
});
document.getElementById("topMemButton").addEventListener("click", function () {
sendAjaxRequest("mem", "ram_current");
});
</script>
</div>
<script>
// needs to be function
function createGauge(elementId, value, title) {
return new JustGage({
id: elementId,
value: parseFloat(value),
min: 0,
max: 100,
title: title
});
}
function updateGauges() {
const cpuPercentage = "{{ container_stats['CPU %'] }}";
const ramPercentage = "{{ container_stats['Memory %'] }}";
createGauge('cpuGauge', cpuPercentage, 'CPU Percentage');
createGauge('ramGauge', ramPercentage, 'RAM Percentage');
}
updateGauges();
</script>
<div class="row">
<div class="col-md-4">
<div class="card mb-4">
<div class="card-header"><h6><i class="bi bi-ethernet"></i> {{ _('Network I/O') }} </h6></div>
<div class="card-body">{{ container_stats['Network I/O'] }}</div>
</div>
</div>
<div class="col-md-4">
<div class="card mb-4">
<div class="card-header"><h6><i class="bi bi-device-ssd"></i> {{ _('Block I/O') }} </h6></div>
<div class="card-body">{{ container_stats['Block I/O'] }}</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header"><h6>{{ _("PIDs") }}</h6></div>
<div class="card-body">{{ container_stats['PIDs'] }}</div>
</div>
</div>
</div>
<script>
document.addEventListener("keydown", function(event) {
if (event.shiftKey) {
if (event.key === "C") {
window.location.href = "/usage";
}
if (event.key === "O") {
window.location.href = "/usage/history";
}
else if (event.key === "L") {
window.location.href = "/usage/logs";
}
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,187 @@
{% extends 'base.html' %}
{% block content %}
<div class="container-xl">
<!-- Account page navigation-->
<nav class="nav nav-line mb-4">
<a class="nav-link" href="/account" target="">{{ _('Profile') }}</a>
<!--<a class="nav-link" href="#">Preferences</a>-->
<a class="nav-link active" href="/account/2fa">{{ _('2FA') }}</a>
{% if 'activity' in enabled_modules %}
<a class="nav-link" href="/activity"><span class="desktop-only">{{ _('Account') }} </span>{{ _('Activity') }}</a>
{% endif %}
{% if 'login_history' in enabled_modules %}
<a class="nav-link" href="/account/login-history"><span class="desktop-only">{{ _('Login') }} </span>{{ _('History') }}</a>
{% endif %}
<!--<a class="nav-link" href="#">Email Notifications</a>-->
</nav>
<div class="row">
{% if success_message %}
<div class="alert alert-success" role="alert">
{{ success_message }}
</div>
{% endif %}
<div class="col-lg-8">
<!-- Security preferences card-->
<div class="card mb-4">
<div class="card-header">{{ _('Two-Factor Authentication') }}</div>
<div class="card-body">
<!-- Account privacy optinos-->
<p class="small text-muted"><a target="_blank" href="https://en.wikipedia.org/wiki/Multi-factor_authentication" rel="noopener noreferrer"><b>T{{ _('wo-factor authentication') }}</b></a> ({{ _('also known as') }} <b>2FA</b>, <b>{{ _('2-step verification') }}</b>, or <b>{{ _('2-phase authentication') }}</b>) {{ _('is a way of adding additional security to your account. 2FA requires you to enter an extra code when you log in or perform some account-sensitive action. The code is generated from an application on your computer or mobile phone.') }} </p>
<p class="small text-muted">{{ _('To enable 2FA for your account you will need an application that manages 2FA codes, such as') }}<a target="_blank" href="https://en.wikipedia.org/wiki/Google_Authenticator" rel="noopener noreferrer">{{ _('Google Authenticator') }}</a>. {{ _('You can install it here:') }}</p>
<p><a target="_blank" href="https://apps.apple.com/us/app/google-authenticator/id388497605" rel="noopener noreferrer"><img src="{{url_for('static', filename='2fa/badge-example-preferred_2x.png')}}" style="height:45px"></a>&nbsp;&nbsp;&nbsp;
<a target="_blank" href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2" rel="noopener noreferrer"><img src="{{url_for('static', filename='2fa/badge_new.png')}}" style="height:45px"></a></p>
<hr class="my-4">
{% if twofa_enabled %}
<h5 class="card-title">{{ _('2FA is currently') }} <b>{{ _('enabled') }}</b> {{ _('for your account.') }}</h5>
<form method="POST" action="{{ url_for('twofa_settings') }}">
<button class="btn btn-warning" type="submit">{{ _('Click to disable 2FA') }}</button>
</form>
{% else %}
<h5 class="card-title">{{ _('2FA is currently') }} <b>{{ _('disabled') }}</b> {{ _('for your account.') }}</h5>
<form method="POST" action="{{ url_for('twofa_settings') }}">
<div class="form-check mb-3" style="display:none;">
<input class="form-check-input" type="checkbox" id="twofa_active" name="twofa_active" checked>
<label class="form-check-label" for="twofa_active" >{{ _('Enable 2FA') }}</label>
</div>
<button class="btn btn-success btn-lg" type="submit">{{ _('Enable 2FA') }}</button>
</form>
{% endif %}
</form>
</div>
</div>
</div>
{% if otp_secret %}
<div class="col-lg-4">
<div class="card mb-4">
<div class="card-header">{{ _('QR code') }}</div>
<div class="card-body">
<!-- Include qrcode.min.js from a CDN -->
<script src="https://cdn.jsdelivr.net/gh/davidshimjs/qrcodejs@latest/qrcode.min.js"></script>
<p class="card-text" id="qrcode"></p>
<script>
var username = '{{ current_username }}';
var otp_secret = '{{ otp_secret }}';
var otpUri = 'otpauth://totp/' + username + '?secret=' + otp_secret + '&issuer=OpenPanel';
var qrcode = new QRCode(document.getElementById('qrcode'), {
text: otpUri,
width: 128,
height: 128,
});
</script>
<a href="#" class="nije-link" id="showLink">{{ _('Display OTP code') }}</a>
<br>
<pre id="initiallyhiddencode" style="display: none;">{{ otp_secret }}</pre>
<script>
// Wait for the document to be fully loaded
document.addEventListener('DOMContentLoaded', function () {
// Get references to the link and pre elements
var showLink = document.getElementById('showLink');
var initiallyHiddenCode = document.getElementById('initiallyhiddencode');
// Attach a click event listener to the link
showLink.addEventListener('click', function (event) {
// Prevent the default behavior of the link
event.preventDefault();
// Toggle the display of the OTP secret code
if (initiallyHiddenCode.style.display === 'none') {
// If currently hidden, show it and populate the OTP secret
initiallyHiddenCode.style.display = 'block';
// Replace this with the actual OTP secret value you want to display
} else {
// If currently shown, hide it
initiallyHiddenCode.style.display = 'none';
}
});
});
</script>
<form method="POST" action="{{ url_for('twofa_settings') }}">
<input class="form-check-input" type="checkbox" id="setup_confirmed" name="setup_confirmed" checked hidden>
{{ _('After configuring the OTP code in the 2FA application, kindly confirm by clicking the button below to permanently delete the OTP secret.') }}
<button class="btn btn-primary" type="submit">{{ _('Confirm') }}</button>
</form>
</div>
</div>
</div>
{% endif %}
</div>
</div>
<style>
.card {
box-shadow: 0 0.15rem 1.75rem 0 rgb(33 40 50 / 15%);
}
.card .card-header {
font-weight: 500;
}
.card-header:first-child {
border-radius: 0.35rem 0.35rem 0 0;
}
.card-header {
padding: 1rem 1.35rem;
margin-bottom: 0;
background-color: rgba(33, 40, 50, 0.03);
border-bottom: 1px solid rgba(33, 40, 50, 0.125);
}
.form-control, .dataTable-input {
display: block;
width: 100%;
padding: 0.875rem 1.125rem;
font-size: 0.875rem;
font-weight: 400;
line-height: 1;
color: #69707a;
background-color: #fff;
background-clip: padding-box;
border: 1px solid #c5ccd6;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
border-radius: 0.35rem;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.nav-borders .nav-link.active {
color: #0061f2;
border-bottom-color: #0061f2;
}
.nav-borders .nav-link {
color: #69707a;
border-bottom-width: 0.125rem;
border-bottom-style: solid;
border-bottom-color: transparent;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
padding-left: 0;
padding-right: 0;
margin-left: 1rem;
margin-right: 1rem;
}
.btn-danger-soft {
color: #000;
background-color: #f1e0e3;
border-color: #f1e0e3;
}
</style>
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,984 @@
<!-- wordpress.html -->
{% extends 'base.html' %}
{% block content %}
{% if domains %}
<style>
.toast-header {
background-color: #192030;
padding: 15px;
font-weight: 600;
font-size: 15px;
margin-bottom: 0;
line-height: 1.4;
color: rgb(255 255 255 / 84%)!important;
}
.no_link {
color: black;
text-decoration: none;
}
.nije-link {
text-decoration: none;
color: black;
border-bottom: 1px dashed black;
}
</style>
<!-- Flash messages -->
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
{% if "Error" in message %}
<script type="module">
const toastMessage = '{{ message }}';
const toast = toaster({
body: toastMessage,
header: `<div class="d-flex align-items-center" style="color: #495057;"><l-i class="bi bi-x-lg" class="me-2" style="color:red;"></l-i> {{ _("WordPress Installation failed.") }}</div>`,
});
</script>
{% else %}
<script type="module">
const toastMessage = '{{ message }}';
const toast = toaster({
body: toastMessage,
header: `<div class="d-flex align-items-center" style="color: #495057;"><l-i class="bi bi-check-lg" class="me-2" style="color:green;"></l-i> {{ _("WordPress successfully installed.") }}</div>`,
});
</script>
{% endif %}
{% endfor %}
{% endif %}
{% endwith %}
<!-- END Flash messages -->
<div class="row">
<script>
// Function to attach event listener to the scanButton
function attachScanButtonListener() {
const scanButton = document.getElementById("scanButton");
const refButton = document.getElementById("refreshData");
if (scanButton) {
scanButton.addEventListener("click", async (ev) => {
ev.preventDefault();
// Update the action to send a scan request
const action = 'scan';
// Display the scanning started toast
const scanStartedToast = toaster({
body: '{{ _("Scanning started...") }}',
className: 'border-0 text-white bg-info',
});
try {
const response = await fetch(`/wordpress/scan?action=${action}`, {
method: 'GET',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
});
// Wait for the scan to finish
const resultText = await response.text();
// Check if installations were found or not
if (resultText.trim() === "Scan completed. Found installations:") {
// Display the message when no installations were found
toaster({
body: '{{ _("No installations found.") }}',
className: 'border-0 text-white bg-success',
});
} else if (resultText.startsWith("Scan completed. Found installations:")) {
// Display the installations if some were found
toaster({
body: resultText,
className: 'border-0 text-white bg-success',
});
} else if (resultText.trim() === "Scan skipped. WordPress installation is currently running.") {
// Display the skipped message as danger
toaster({
body: '{{ _("Scan skipped. WordPress installation is currently running.") }}',
className: 'border-0 text-white bg-danger',
});
} else {
// Display the entire response if it doesn't match the expected format
toaster({
body: resultText,
className: 'border-0 text-white bg-success',
});
}
} catch (error) {
console.error('Error:', error);
// Display error message in a new toast
toaster({
body: 'Error occurred during scan.',
className: 'border-0 text-white bg-danger',
});
}
});
}
if (refButton) {
refButton.addEventListener("click", async (ev) => {
ev.preventDefault();
// Display the scanning started toast
const refStartedToast = toaster({
body: '{{ _("Data refresh in progress...") }}',
className: 'border-0 text-white bg-info',
});
try {
const response = await fetch(`/wordpress/reload_data`, {
method: 'GET',
});
const resultText = await response.text();
if (resultText.startsWith("Scan completed. Found installations:")) {
// Display the installations if some were found
toaster({
body: "{{ _('Refresh completed.') }}",
className: 'border-0 text-white bg-success',
});
}
} catch (error) {
console.error('Error:', error);
// Display error message in a new toast
toaster({
body: 'Error occurred during refresh.',
className: 'border-0 text-white bg-danger',
});
}
});
}
}
// Attach event listener to the scanButton initially
attachScanButtonListener();
// Add a click event listener to scanButton2
var scanButton2 = document.getElementById('scanButton2');
if (scanButton2) {
scanButton2.addEventListener('click', async () => {
const scanButton = document.getElementById("scanButton");
if (scanButton) {
scanButton.click();
}
});
}
</script>
<style>
@keyframes rotate {
to {
--angle: 360deg
}
}
@property --angle {
syntax: "<angle>";
initial-value: 0deg;
inherits: false
}
.CrawlerStatusCard.active {
animation: rotate 2s linear infinite;
background: hsla(0,0%,100%,.5);
border: 2px solid transparent;
border-image: conic-gradient(from var(--angle),transparent 0deg 90deg,transparent 90deg 180deg,transparent 180deg 270deg,#0077bc 270deg 1turn) 1 stretch;
}
</style>
<div class="collapse mb-2" id="collapseExample">
<div id="toAddActive" class="card CrawlerStatusCard card-body">
<!-- Form for adding new containers -->
<div class="container">
<a href="#" class="nije-link" id="cancelLink" style="display: none;"><i class="bi bi-x-lg" style="right: 15px;top: 15px;position: absolute;color: black;padding: 6px 10px;border-radius: 50px;"></i></a>
<div class="row">
<div class="col-md-6 offset-md-3">
<h2 class="mb-3"><i class="bi bi-wordpress"></i> {{ _("Install WordPress") }}</h2>
<p>{{ _("Install WordPress on an existing domain.") }}</p>
<form method="post" id="installForm" action="/wordpress/install">
<div class="form-group row">
<div class="col-6">
<label for="website_name" class="form-label">{{ _("Website Name:") }}</label>
<input type="text" class="form-control" name="website_name" id="website_name" value="My Blog" required>
</div>
<div class="col-6">
<label for="site_description" class="form-label">{{ _("Site Description:") }}</label>
<input type="text" class="form-control" name="site_description" id="site_description" value="My WordPress Blog" required>
</div>
</div>
<div class="form-group">
<label for="domain_id" class="form-label">{{ _("Domain:") }}</label>
<div class="input-group">
</select>
<select class="form-select" name="domain_id" id="domain_id">
<option selected disabled value="">{{ _("Select a domain") }}</option>
{% for domain in domains %}
<option class="punycode" value="{{ domain.domain_id }}">{{ domain.domain_url }}</option>
{% endfor %}
</select>
<div class="input-group-append" style="width:30%;">
<input type="text" class="form-control" name="subdirectory" id="subdirectory" placeholder="subfolder">
</div>
</div>
</div>
<div class="form-group">
<label for="admin_email">{{ _("Admin Email:") }}</label>
<input type="email" class="form-control" name="admin_email" id="admin_email" required>
</div>
<script type="module">
$(document).ready(function() {
// Check if the URL contains the parameter "install"
const urlParams = new URLSearchParams(window.location.search);
const installParam = urlParams.get('install');
if (installParam || window.location.hash === '#install') {
// Show the Bootstrap collapsible element
$("#collapseExample").collapse('show');
}
// Add event listener to the dropdown
$("#domain_id").change(function() {
// Get the selected domain URL
var selectedDomain = $("#domain_id option:selected").text();
// Update the admin email input value
var adminEmailInput = $("#admin_email");
var currentAdminEmail = adminEmailInput.val();
var atIndex = currentAdminEmail.indexOf('@');
if (atIndex !== -1) {
// If '@' exists in the email, replace the part after it with the selected domain
adminEmailInput.val(currentAdminEmail.substring(0, atIndex + 1) + selectedDomain);
} else {
// If '@' doesn't exist in the email, add the selected domain after 'admin@'
adminEmailInput.val('admin@' + selectedDomain);
}
});
// Add event listener to the "Install WordPress" button to toggle form and jumbotron
$("#collapseExample").on('shown.bs.collapse', function () {
$("#jumbotronSection").hide();
$("#cancelLink").show();
});
// Add event listener to the "Cancel" link to toggle form and jumbotron
$("#cancelLink").click(function() {
$("#collapseExample").collapse('hide');
$("#jumbotronSection").show();
$("#cancelLink").hide();
});
});
var selectedDomain = $("#domain_id option:selected").text();
// Check if the selected domain is the first option (placeholder)
if ($("#domain_id option:selected").index() !== 0) {
// Update the admin email input value
var adminEmailInput = $("#admin_email");
var currentAdminEmail = adminEmailInput.val();
var atIndex = currentAdminEmail.indexOf('@');
if (atIndex !== -1) {
// If '@' exists in the email, replace the part after it with the selected domain
adminEmailInput.val(currentAdminEmail.substring(0, atIndex + 1) + selectedDomain);
} else {
// If '@' doesn't exist in the email, add the selected domain after 'admin@'
adminEmailInput.val('admin@' + selectedDomain);
}
}
</script>
<div class="form-group row">
<div class="col-md-6">
<label for="admin_username" class="form-label">{{ _("Admin Username:") }}</label>
<input type="text" class="form-control" name="admin_username" id="admin_username" required>
</div>
<div class="col-md-6">
<label for="admin_password" class="form-label">{{ _("Admin Password:") }}</label>
<div class="input-group">
<input type="password" class="form-control" name="admin_password" id="admin_password" required>
<div class="input-group-append">
<button class="btn btn-outline-success" type="button" id="generatePassword">
{{ _("Generate") }}
</button>
<button class="btn btn-outline-secondary" type="button" id="togglePassword">
<i class="bi bi-eye"></i>
</button>
</div>
</div>
</div>
</div>
<script type="module">
function generateRandomUsername(length) {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let result = "";
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * charset.length);
result += charset.charAt(randomIndex);
}
return result;
}
function generateRandomStrongPassword(length) {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+";
let result = "";
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * charset.length);
result += charset.charAt(randomIndex);
}
return result;
}
function generateInitiallyUsernameAndPassword() {
const generatedUsername = generateRandomUsername(8);
const generatedPassword = generateRandomStrongPassword(16);
document.getElementById("admin_username").value = generatedUsername;
document.getElementById("admin_password").value = generatedPassword;
};
generateInitiallyUsernameAndPassword();
document.getElementById("generatePassword").addEventListener("click", function() {
const generatedPassword = generateRandomStrongPassword(16);
document.getElementById("admin_password").value = generatedPassword;
const passwordField = document.getElementById("admin_password");
if (passwordField.type === "password") {
passwordField.type = "text";
}
});
document.getElementById("togglePassword").addEventListener("click", function() {
const passwordField = document.getElementById("admin_password");
if (passwordField.type === "password") {
passwordField.type = "text";
} else {
passwordField.type = "password";
}
});
</script>
<div class="form-group">
<label for="wordpress_version" class="form-label">WordPress Version:</label>
<div class="form-field">
<select class="form-select" name="wordpress_version" id="wordpress_version">
</select>
</div>
</div>
<br>
<button type="submit" class="btn btn-primary" id="installButton">{{ _("Start Installation") }}</button>
</form>
<div id="statusMessage"></div>
<script type="module">
document.getElementById("installForm").addEventListener("submit", function(event) {
event.preventDefault();
var selectedDomain = $("#domain_id option:selected").text();
// Check if the first option (placeholder) is selected
if ($("#domain_id option:selected").index() === 0) {
event.preventDefault(); // Prevent form submission
toaster({
body: `{{ _("Please select a domain name.") }}`,
className: 'border-0 text-white bg-danger',
});
return;
}
// Hide the installation form
const installForm = document.getElementById("installForm");
installForm.style.display = "none"; // Hide the form
// Show the active state
document.getElementById("toAddActive").classList.add("active");
// Disable the button to prevent multiple clicks
const button = document.getElementById("installButton");
button.disabled = true;
button.innerText = "Installing..."; // Initial message
// Clear previous status messages
const statusMessageDiv = document.getElementById("statusMessage");
statusMessageDiv.innerText = "";
// Start fetching for updates
const formData = new FormData(this); // Get the form data
const responseStream = fetch('/wordpress/install', {
method: 'POST',
body: formData,
});
responseStream
.then(response => {
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
function read() {
reader.read().then(({ done, value }) => {
if (done) {
showCompletion();
return;
}
const message = decoder.decode(value, { stream: true });
const jsonMessages = message.split('\n').filter(Boolean); // Split lines and remove empty
jsonMessages.forEach(jsonMessage => {
try {
const data = JSON.parse(jsonMessage);
if (data.status) {
statusMessageDiv.innerText += data.status + "\n"; // append
} else if (data.error) {
toaster({
body: data.error,
className: 'border-0 text-white bg-danger',
});
showCompletion(); // redirect
}
} catch (e) {
//console.error("Error parsing JSON:", e);
}
});
read(); // Continue
});
}
read(); // Start
})
.catch(error => {
statusMessageDiv.innerText += "Error occurred while processing.\n";
console.error("Fetch error:", error);
toaster({
body: error,
className: 'border-0 text-white bg-danger',
});
showCompletion(); // redirect
});
function showCompletion() {
statusMessageDiv.style.display = "block";
window.location.href = '/wordpress';
}
});
</script>
<script type="module">
// Fetch the WordPress versions from the API
fetch('https://api.wordpress.org/core/stable-check/1.0/')
.then(response => response.json())
.then(data => {
// Filter out the versions with "latest" and "outdated" status
const filteredVersions = Object.entries(data)
.filter(([version, status]) => status !== 'insecure')
.map(([version, _]) => version)
.sort((a, b) => compareVersions(b, a)) // Sort in descending order
// Take the latest 10 versions
const latestVersions = filteredVersions.slice(0, 10);
// Populate the select dropdown with options
const selectElement = document.getElementById('wordpress_version');
latestVersions.forEach(version => {
const option = document.createElement('option');
option.value = version;
option.textContent = `${version}`;
selectElement.appendChild(option);
});
})
.catch(error => {
console.error('Error fetching WordPress versions:', error);
// In case of an error, you might want to display a default option
const selectElement = document.getElementById('wordpress_version');
const option = document.createElement('option');
option.value = ''; // Set a value for the default option if needed
option.textContent = '{{ _("Error fetching versions") }}';
selectElement.appendChild(option);
});
// Function to compare two version strings
function compareVersions(a, b) {
const versionA = a.split('.').map(Number);
const versionB = b.split('.').map(Number);
for (let i = 0; i < Math.max(versionA.length, versionB.length); i++) {
const partA = versionA[i] || 0;
const partB = versionB[i] || 0;
if (partA < partB) return -1;
if (partA > partB) return 1;
}
return 0;
}
</script>
</div>
</div>
</div>
</div>
</div>
{% if data %}
{% if view_mode == 'table' %}
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>{{ _("Domain") }}</th>
<th>{{ _("WordPress Version") }}</th>
<th>{{ _("Admin Email") }}</th>
<th>{{ _("Created on") }}</th>
<th>{{ _("Actions") }}</th>
</tr>
</thead>
<tbody>
{% for row in data %}
<div class="modal fade" id="removeModal{{ row[1] }}" tabindex="-1" role="dialog" aria-labelledby="removeModalLabel{{ row[1] }}" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="removeModalLabel{{ row[0] }}">{{ row[0] }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body row">
<div class="col-md-6">
<h4>{{ _("Delete WordPress website") }}</h4>
<p>{{ _("This will irreversibly delete the website, permanently deleting all files and database.") }}</p>
<button type="button" class="btn btn-danger" onclick="confirmRemove('{{ row[1] }}')">{{ _("Uninstall") }}</button>
</div>
<div class="col-md-6">
<h4>{{ _("Remove from WP Manager") }}</h4>
<p>{{ _("This will just remove the installation from WP Manager but keep the files and database.") }}</p>
<button type="button" class="btn btn-warning" onclick="confirmDetach('{{ row[1] }}')">{{ _("Detach") }}</button>
</div>
</div>
<div class="modal-footer">
</div>
</div>
</div>
</div>
{% set domain_url = row[0] %}
<tr>
<td><a class="punycode no_link domain_link" href="http://{{ row[0] }}" target="_blank"><img src="https://www.google.com/s2/favicons?domain={{ row[0] }}" alt="{{ row[0] }} Favicon" style="width:16px; height:16px; margin-right:5px;">
{{ row[0] }} <i class="bi bi-box-arrow-up-right"></i></a></td>
<td>{{ row[3] }}</td>
<td>{{ row[2] }}</td>
<td>{{ row[4] }}</td>
<td>
<a href="#" class="btn btn-primary" onclick="handleLoginClick('{{ row[0] }}')"> {{ _("Login as Admin") }} <i class="bi bi-box-arrow-in-right"></i></a>
<a class="btn btn-secondary mx-2" href="/website?domain={{ row[0] }}">{{ _("Manage") }}</a>
<button type="button" class="btn btn-danger" style="border: 0px;" data-bs-toggle="modal" data-bs-target="#removeModal{{ row[1] }}"><i class="bi bi-trash3"></i> {{ _("Remove") }}</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% if view_mode == 'cards' %}
<style>
/* Style for the container */
.image-container {
position: relative;
display: inline-block;
}
/* Style for the button */
.center-button {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: none; /* Hide the button by default */
z-index: 1; /* Ensure the button appears above the image */
}
/* Show the button when hovering over the image container */
.image-container:hover .center-button {
display: block;
}
.card {
border: none;
border-radius: 10px
}
.c-details span {
font-weight: 300;
font-size: 13px
}
.icon {
width: 50px;
height: 50px;
background-color: #eee;
border-radius: 15px;
display: flex;
align-items: center;
justify-content: center;
font-size: 39px
}
.badge span {
background-color: black;
width: 80px;
height: 20px;
padding-bottom: 3px;
border-radius: 5px;
display: flex;
color: white;
justify-content: center;
align-items: center
}
.text1 {
font-size: 14px;
font-weight: 600
}
.text2 {
color: #a5aec0
}
a.close_button {
right: 5px;top: 10px;position: absolute;color: white;padding: 0px 4px;background: indianred;border-radius: 50px; z-index:1;
}
a.close_button:hover {
background: red;
}
</style>
<div class="container mb-3">
<div class="row">
{% for row in data %}
<div class="modal fade" id="removeModal{{ row[6] }}" tabindex="-1" role="dialog" aria-labelledby="removeModalLabel{{ row[6] }}" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="removeModalLabel{{ row[6] }}">{{ row[0] }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body row">
<div class="col-md-6">
<h4>{{ _("Delete WordPress website") }}</h4>
<p>{{ _("This will irreversibly delete the website, permanently deleting all files and database.") }}</p>
<button type="button" class="btn btn-danger" onclick="confirmRemove('{{ row[6] }}')">{{ _("Uninstall") }}</button>
</div>
<div class="col-md-6">
<h4>{{ _("Remove from WP Manager") }}</h4>
<p>{{ _("This will just remove the installation from WP Manager but keep the files and database.") }}</p>
<button type="button" class="btn btn-warning" onclick="confirmDetach('{{ row[6] }}')">{{ _("Detach") }}</button>
</div>
</div>
<div class="modal-footer">
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card bg-light mb-2">
<a href="#" class="close_button" data-bs-toggle="modal" data-bs-target="#removeModal{{ row[6] }}"><i class="bi bi-x-lg" style=""></i></a>
<div class="mt-0">
<div class="image-container">
<a href="/website?domain={{ row[0] }}">
<img
id="screenshot-image-{{ row[0] }}"
src="/static/images/placeholder.svg"
alt="Screenshot of {{ row[0] }}"
class="img-fluid"
/>
</a>
<a href="/website?domain={{ row[0] }}" class="center-button btn btn-dark">
<i class="bi bi-sliders2-vertical"></i> {{ _("Manage") }}
</a>
</div>
</div>
<div class="d-flex p-2 justify-content-between">
<div class="d-flex flex-row align-items-center">
<div class="icon"> <i class="bi bi-wordpress"></i> </div>
<div class="ms-2 c-details">
<h6 class="punycode mb-0"><a href="http://{{ row[0] }}" target="_blank" class="nije-link">{{ row[0] }}</a></h6> <span>{{ _("WordPress") }} {{ row[3] }}</span>
</div>
</div>
<div class="badge" style="display: -webkit-box;"><button type="button" style="width:100%;" class="btn btn-sm btn-outline-primary" onclick="handleLoginClick('{{ row[0] }}')"> {{ _("Login as Admin") }} <i class="bi bi-box-arrow-in-right"></i></button> <!--div class="dropdown" style="vertical-align: middle; display: inline;">
<a href="" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-three-dots-vertical" style="font-size: 1.3125rem;"></i>
</a>
<ul class="dropdown-menu" aria-labelledby="more-actions">
<li><a class="dropdown-item" href="#">PHP Version</a></li>
<li><a class="dropdown-item" href="#">SSL Security</a></li>
<li><a class="dropdown-item" href="#">SSL Security</a></li>
<hr>
<li><a class="dropdown-item" href="#">Refresh Preview</a></li>
</ul>
</div--></div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<script type="module">
// Function to update a screenshot image
function updateScreenshot(screenshotImage, screenshotURL) {
var imageLoader = new Image();
imageLoader.src = screenshotURL;
imageLoader.onload = function() {
screenshotImage.src = screenshotURL;
};
}
// Function to update all screenshot images
function updateAllScreenshots() {
{% for row in data %}
var screenshotImage = document.getElementById("screenshot-image-{{ row[0] }}");
var screenshotURL = "/screenshot/{{ row[0] }}";
updateScreenshot(screenshotImage, screenshotURL);
{% endfor %}
}
updateAllScreenshots();
</script>
{% endif %}
<script type="module">
// Function to confirm removal
function confirmRemove(id) {
// Send an AJAX request to the server to remove WordPress
var xhr = new XMLHttpRequest();
xhr.open('POST', '/wordpress/remove', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
// Reload the page to update the table
location.reload();
} else {
alert('{{ _("An error occurred while removing WordPress.") }}');
}
}
};
xhr.send('id=' + id);
}
// Function to confirm detachment
function confirmDetach(id) {
// Send an AJAX request to the server to remove WordPress
var xhr = new XMLHttpRequest();
xhr.open('POST', '/wordpress/detach', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
// Redirect to wp manager page
window.location.href = '/wordpress';
} else {
alert('{{ _("An error occurred while detaching WordPress.") }}');
}
}
};
xhr.send('id=' + id);
}
</script>
<script>
// Function to handle the button click
function handleLoginClick(domain) {
// Send an AJAX request to the server to get the login link
var xhr = new XMLHttpRequest();
xhr.open('GET', '/get_login_link?domain=' + domain, true);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
var response = JSON.parse(xhr.responseText);
if (response && response.login_link) {
// Open the login link in a new tab
window.open(response.login_link, '_blank');
}
}
};
xhr.send();
}
</script>
</div>
<style>
.nije-link {
text-decoration: none;
color: black;
border-bottom: 1px dashed black;
}
</style>
{% else %}
<!-- Display jumbotron for no wp installations -->
<div class="jumbotron text-center mt-5" id="jumbotronSection" style="min-height: 70vh;display:block;">
<h1>{{ _("No WordPress Installations") }}</h1>
<p>{{ _("You don't have WordPress sites connected yet. Install a new WordPress site or scan to find existing ones.") }}</p>
<button class="btn btn-lg btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#collapseExample" aria-expanded="false" aria-controls="collapseExample">
<i class="bi bi-wordpress"></i> {{ _("Install WordPress") }}
</button>
<p class="mb-0">{{ _("or") }}</p>
<button id="scanButton2" class="btn btn-outline">
<i class="bi bi-search"></i> {{ _("Scan for existing Installations") }}
</button>
</div>
{% endif %}
{% else %}
<!-- Display jumbotron for no domains -->
<div class="jumbotron text-center mt-5" id="jumbotronSection">
<h1>{{ _("No Domains") }}</h1>
<p>{{ _("Add a domain name first in order to install WordPress.") }}</p>
<a href="/domains#add-new" class="btn btn-lg btn-primary">
<i class="bi bi-plus-lg"></i> {{ _("Add a Domain Name") }}
</a>
</div>
{% endif %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/punycode/2.1.1/punycode.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
var punycodeElements = document.querySelectorAll(".punycode");
punycodeElements.forEach(function(element) {
element.textContent = punycode.toUnicode(element.textContent);
});
});
</script>
</section>
<footer class="main-footer btn-toolbar" role="toolbar">
<div class="btn-group" role="group" aria-label="Status">
</div>
<div class="ms-auto" role="group" aria-label="Actions">
<button class="btn btn-transparent" type="button" aria-expanded="false" id="refreshData">
<i class="bi bi-arrow-counterclockwise"></i> {{ _('Refresh') }}<span class="desktop-only"> {{ _('Data') }}</span>
</button>
<button class="btn btn-primary mx-2" type="button" data-bs-toggle="collapse" data-bs-target="#collapseExample" aria-expanded="false" aria-controls="collapseExample">
<i class="bi bi-wordpress"></i> {{ _('Install') }}<span class="desktop-only"> {{ _('WordPress') }}</span>
</button>
<button id="scanButton" class="btn btn-dark" type="button">
<i class="bi bi-search"></i> {{ _('Scan') }}
</button>
<span class="desktop-only">{% if view_mode == 'cards' %}
<a href="{{ url_for('list_wordpress', view='table') }}" data-bs-toggle="tooltip" data-bs-placement="top" title="{{ _('Switch to List View - perfect for those with many Websites.') }}" class="btn btn-outline" style="padding-left: 5px; padding-right: 5px;"><i class="bi bi-table"></i></a>
{% endif %}
{% if view_mode == 'table' %}
<a href="{{ url_for('list_wordpress', view='cards') }}" data-bs-toggle="tooltip" data-bs-placement="top" title="{{ _('Switch to Grid View - a visual representation of your Websites and their content.') }}" class="btn btn-outline" style="padding-left: 5px; padding-right: 5px;"><i class="bi bi-view-list"></i></a>
{% endif %}</span>
<script>
// Add an event listener to the links to hide tooltip as its buggy!
document.addEventListener('DOMContentLoaded', function () {
var links = document.querySelectorAll('.desktop-only a');
links.forEach(function (link) {
link.addEventListener('click', function () {
// Hide the tooltip when the link is clicked
var tooltip = new bootstrap.Tooltip(link);
tooltip.hide();
});
});
});
</script>
</div>
</footer>
{% endblock %}

View File

@@ -1 +0,0 @@
[tabler](https://tabler.io/admin-template) is the awesome default theme for OpenAdmin ✌️

File diff suppressed because it is too large Load Diff

1123
templates/tabler/base.html Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,61 @@
{% extends 'base.html' %}
{% block content %}
<!-- Page header -->
<div class="page-header mt-0 d-print-none">
<div class="container-xl">
<div class="row g-2 align-items-center">
<div class="col">
<!-- Page pre-title -->
<div class="page-pretitle">
Settings
</div>
<h2 class="page-title">
Cluster
</h2>
</div>
<!-- Page title actions -->
<div class="col-auto ms-auto d-print-none">
<div class="btn-list">
<button type="submit" class="btn btn-primary">Save</button>
</div>
</div>
</div>
</div>
</div>
<!-- Page body -->
<div class="page-body">
<div class="table-responsive">
<table id="docker-contexts-table" class="table table-vcenter table-mobile-md card-table">
<thead>
<tr>
<th>Server</th>
<!--<th>Description</th>-->
<th>Docker Endpoint</th>
<th>Default</th>
<!--<th>Error</th>-->
</tr>
</thead>
<tbody>
<!-- Rows will be dynamically loaded here -->
</tbody>
</table>
</div>
</div>
<script src="{{ url_for('static', filename='pages/cluster.js') }}" defer></script>
{% endblock %}

View File

@@ -0,0 +1,156 @@
<!-- Content of cpu_per_core_usage.html -->
<style>
.dismiss-cpu_per_core_usage-button {
display: none;
}
#cpu_per_core_usage:hover .dismiss-cpu_per_core_usage-button {
display: inline-block;
}
</style>
<script>
$(document).ready(function() {
function fetchData() {
$.ajax({
url: '/json/cpu-usage',
type: 'GET',
dataType: 'json',
async: true,
success: function(data) {
var container = $('.container2');
container.empty();
$.each(data, function(core, usage) {
var coreInfo = '<div><span>' + usage + '%</span></div>';
var coreDiv = $(coreInfo);
if (usage >= 90) {
coreDiv.css('background-color', 'crimson');
} else if (usage >= 80) {
coreDiv.css('background-color', 'antiquewhite');
}
container.append(coreDiv);
});
},
error: function() {
// Handle any errors here
console.log('Error fetching CPU usage data.');
}
});
}
fetchData();
setInterval(fetchData, 1000);
});
</script>
<div class="col g-3">
<div id="cpu_per_core_usage" class="card card-one">
<div class="card-header">
<div>
<form action="{{ url_for('dismiss_dashboard_widget', template_name='cpu_per_core_usage') }}" method="post">
<h6 class="card-title">CPU <button data-bs-toggle="tooltip" data-bs-placement="top" aria-label="Hide and never show cpu usage per core again" data-bs-original-title="Hide and never show cpu usage per core again" class="dismiss-cpu_per_core_usage-button btn btn-sm" type="submit">Dismiss</button></h6>
<p class="card-subtitle">Real-time usage per cpu core.</p>
</form>
</div>
<nav class="nav nav-icon nav-icon-sm ms-auto"><a data-bs-toggle="collapse" class="nav-link collapsed" href="#cpu_info" role="button" aria-expanded="false" aria-controls="Factor"><i class="bi bi-question-circle"></i></a></nav> </div>
<div class="card-body" style="padding:0">
<div class="collapse p-2" id="cpu_info" style="">
<p>The data being displayed is the usage percentage of each CPU core, with one hexagon per CPU core, and background colors indicating usage levels (default for 0-79%, orange for 80-89%, and red for 90-100%).</p>
<p>The CPU usage percentage represents the amount of the CPU's processing power that is currently being utilized. It indicates how much of the CPU's capacity is in use at a specific moment. For example, a CPU usage of 50% means that
the CPU is operating at half of its maximum processing capacity, while 100% usage indicates that the CPU is fully utilized, and there may be resource constraints or performance issues.</p>
<p>The data is auto-refreshed every 1 second to provide real-time updates.</p>
</div>
<div class="main2">
<a href="{{ url_for('server_cpu_usage') }}">
<div class="container2"></div>
</a>
</div>
<style>
.main2 {
display: flex;
--s: 50px;
--m: 2px;
--f: calc(1.732 * var(--s) + 4 * var(--m) - 1px)
}
.container2 {
font-size: 0;
margin-bottom: 2rem
}
.container2 div {
position: relative;
width: var(--s);
margin: var(--m);
height: calc(var(--s)*1.1547);
display: inline-block;
font-size: initial;
clip-path: polygon(0 25%, 0 75%, 50% 100%, 100% 75%, 100% 25%, 50% 0);
background: #f0f8ff;
margin-bottom: calc(var(--m) - var(--s)*.2885)
}
.container2 div span {
vertical-align: middle;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 12px;
font-weight: 700;
color: #000
}
.container2::before {
content: "";
width: calc(var(--s)/ 2 + var(--m));
float: left;
height: 120%;
shape-outside: repeating-linear-gradient(#0000 0 calc(var(--f) - 3px), #000 0 var(--f))
}
</style>
</div>
</div>
<div style="display:none" class="mt-3 card card-one">
<div class="card-header">
<div>
<h6 class="card-title">Servers List</h6>
<p class="card-subtitle">List of Docker contexts.</p>
</div>
<nav class="nav nav-icon nav-icon-sm ms-auto"><a data-bs-toggle="collapse" class="nav-link collapsed" href="#cluster_info" role="button" aria-expanded="false" aria-controls="cluster_info"><i class="bi bi-question-circle"></i></a></nav>
</div>
<div class="card-body" style="padding:0">
<div class="collapse p-2" id="cluster_info" style="">
<p>This table displays information about Docker contexts, which represent different servers or Docker environments. Each row in the table corresponds to a Docker context and provides details about the context's configuration and status.</p>
<p></p>
</div>
<table id="docker-contexts-table" class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>Docker Endpoint</th>
<th>Default</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>

View File

@@ -0,0 +1,687 @@
{% extends 'base.html' %} {% block content %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.css">
<style>
.active-status {
color: green
}
.inactive-status {
color: red !important;
font-weight: 700
}
.other-status {
color: orange !important
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"></script>
<div class="row g-3 mb-0">
<div class="col-sm-2">
<a href="/users" style="text-decoration:none;">
<div class="card card-one">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto"><span class="bg-primary-lt text-white avatar"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-users" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 7m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0"/><path d="M3 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/><path d="M21 21v-2a4 4 0 0 0 -3 -3.85"/></svg></span></div>
<div class="col">
<div class="font-weight-medium">{{ user_count }}{% if license_type == "Community" %} / 3{% endif %}</div>
<div class="text-muted">Users</div>
</div>
</div>
</div>
</div>
</a>
</div>
<div class="col-sm-2">
<a href="/backups" style="text-decoration:none;">
<div class="card card-one">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto"><span class="bg-primary-lt text-white avatar"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-cloud-upload" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M7 18a4.6 4.4 0 0 1 0 -9a5 4.5 0 0 1 11 2h1a3.5 3.5 0 0 1 0 7h-1"/><path d="M9 15l3 -3l3 3"/><path d="M12 12l0 9"/></svg></span></div>
<div class="col">
<div class="font-weight-medium">{{backup_jobs}}</div>
<div class="text-muted">Backup Jobs</div>
</div>
</div>
</div>
</div>
</a>
</div>
<div class="col-sm-2">
<a href="/plans" style="text-decoration:none;">
<div class="card card-one">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto"><span class="bg-primary-lt text-white avatar"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-packages" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M7 16.5l-5 -3l5 -3l5 3v5.5l-5 3z"/><path d="M2 13.5v5.5l5 3"/><path d="M7 16.545l5 -3.03"/><path d="M17 16.5l-5 -3l5 -3l5 3v5.5l-5 3z"/><path d="M12 19l5 3"/><path d="M17 16.5l5 -3"/><path d="M12 13.5v-5.5l-5 -3l5 -3l5 3v5.5"/><path d="M7 5.03v5.455"/><path d="M12 8l5 -3"/></svg></span></div>
<div class="col">
<div class="font-weight-medium">{{ plan_count }}</div>
<div class="text-muted">Plans</div>
</div>
</div>
</div>
</div>
</a>
</div>
<div class="col-sm-3">
<a href="{{ url_for('server_cpu_usage') }}" style="text-decoration:none;">
<div class="card card-one">
<div id="serverloadIndicator" class="card-status-top bg-primary-lt"></div>
<div class="card-body">
<div class="d-flex align-items-center">
<div class="subheader">Load Averages</div>
<div class="ms-auto lh-1">
<div class="dropdown">
<a class="dropdown-toggle text-secondary" href="javascript:void(0)" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Actions</a>
<div class="dropdown-menu dropdown-menu-end" style="">
<a class="dropdown-item" href="{{ url_for('server_cpu_usage') }}"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-list-details"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M13 5h8" /><path d="M13 9h5" /><path d="M13 15h8" /><path d="M13 19h5" /><path d="M3 4m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z" /><path d="M3 14m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z" /></svg>&nbsp; View Processes</a>
</div>
</div>
</div>
</div>
<div class="d-flex align-items-baseline">
<div class="me-auto">
<div class="server-load">
<div class="h3 mb-0 me-2 load"><span class="load-indicator" id="load-indicator"></span>&nbsp;<b><span id="load1min">...</span>&nbsp;<span id="load5min">...</span>&nbsp;<span id="load15min">...</span></b></div>
</div>
</div>
</div>
</div>
</div>
</a>
</div>
<div class="col-sm-3">
<a href="{{ url_for('server_memory_usage') }}" style="text-decoration:none;">
<div class="card card-one">
<div id="ramIndicator" class="card-status-top bg-primary-lt"></div>
<div class="card-body">
<div class="d-flex align-items-center">
<div class="subheader">Memory Usage</div>
<div class="ms-auto lh-1">
<div class="dropdown">
<a class="dropdown-toggle text-secondary" href="javascript:void(0)" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Actions</a>
<div class="dropdown-menu dropdown-menu-end" style="">
<a class="dropdown-item" href="/server/memory_usage"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-list-details"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M13 5h8" /><path d="M13 9h5" /><path d="M13 15h8" /><path d="M13 19h5" /><path d="M3 4m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z" /><path d="M3 14m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z" /></svg>&nbsp; View Processes</a>
<a class="dropdown-item" href="javascript:void(0)" id="clear-cache"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-rocket"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 13a8 8 0 0 1 7 7a6 6 0 0 0 3 -5a9 9 0 0 0 6 -8a3 3 0 0 0 -3 -3a9 9 0 0 0 -8 6a6 6 0 0 0 -5 3" /><path d="M7 14a6 6 0 0 0 -3 6a6 6 0 0 0 6 -3" /><path d="M15 9m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /></svg>&nbsp; Clear Cached RAM</a>
<a class="d-none dropdown-item" href="/services/resources"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-badge-sd"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 5m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z" /><path d="M14 9v6h1a2 2 0 0 0 2 -2v-2a2 2 0 0 0 -2 -2h-1z" /><path d="M7 14.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75" /></svg>&nbsp; Create SWAP</a>
</div>
</div>
</div>
</div>
<div class="d-flex align-items-baseline">
<div class="h3 mb-0 me-2 ram-info" id="human-readable-info"></div>
<div class="me-auto">
<span id="ramIconColor" class="bg-primary-lt text-white d-inline-flex align-items-center lh-1"></span>
</div>
</div>
</div>
</div>
</a>
</div>
<script>
$(document).ready(function() {
$('#clear-cache').on('click', function(event) {
event.preventDefault(); // Prevent the default behavior to keep the dropdown open
var dropdownItem = $(this);
$.ajax({
url: '/server/memory_usage/drop',
type: 'POST',
success: function(response) {
// Update only the "Clear Cached RAM" item
dropdownItem.html('<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-rocket"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 13a8 8 0 0 1 7 7a6 6 0 0 0 3 -5a9 9 0 0 0 6 -8a3 3 0 0 0 -3 -3a9 9 0 0 0 -8 6a6 6 0 0 0 -5 3" /><path d="M7 14a6 6 0 0 0 -3 6a6 6 0 0 0 6 -3" /><path d="M15 9m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /></svg>&nbsp;' + response.message);
},
error: function(xhr, status, error) {
// Handle error
dropdownItem.html('<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-rocket"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 13a8 8 0 0 1 7 7a6 6 0 0 0 3 -5a9 9 0 0 0 6 -8a3 3 0 0 0 -3 -3a9 9 0 0 0 -8 6a6 6 0 0 0 -5 3" /><path d="M7 14a6 6 0 0 0 -3 6a6 6 0 0 0 6 -3" /><path d="M15 9m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /></svg>&nbsp; Error: ' + xhr.responseJSON.message);
}
});
});
});
</script>
</div>
<div class="row">
<div class="row col-md-12">
<div class="col-md-4 g-3">
<div class="col-12">
<div class="card" style="height:28rem">
<div class="card-header">
<div>
<h6 class="card-title">User Activity</h6>
<p class="card-subtitle">Real-time user activity log.</p>
</div>
</div>
<div id="activity-table" class="card-body card-body-scrollable card-body-scrollable-shadow">
<div class="divide-y"><span id="shouldbehidden">Users activity log will appear here.</span></div>
</div>
</div>
</div>
</div>
{% if history_usage_graphs %}
<!-- Include the history_usage_graphs.html template -->
{% include 'dashboard/usage_graphs.html' %}
{% endif %}
{% if get_started %}
<!-- Include the get_started.html template -->
{% include 'dashboard/get_started.html' %}
{% endif %}
<div class="col-md-4 g-3">
<div class="card card-one" style="height:28rem">
<div class="card-header">
<div>
<h6 class="card-title">Services Status</h6>
<p class="card-subtitle">The Service Status table only reports monitored services.</p>
</div>
<nav class="nav nav-icon nav-icon-sm ms-auto"><a data-bs-toggle="collapse" class="nav-link collapsed" href="#services_info" role="button" aria-expanded="false" aria-controls="services_info"><i class="bi bi-question-circle"></i></a></nav>
</div>
<div class="card-body" style="padding:0;align-content: space-between;display: grid;">
<div class="collapse p-2" id="services_info" style="">
<p>Display current status of all monitored system services and docker containers. To add custom services to the list, edit the <code>/etc/openpanel/openadmin/config/services.json</code> file.
</p>
<p>
<ul>
<li><span class="legend me-2 bg-success"></span><span>- is currently active and running.</span></li>
<li><span class="legend me-2 bg-danger"></span><span>- not currently active, either failed or stopped.</span></li>
<li><span class="legend me-2 bg-primary"></span><span>- not yet started, will auto-start when user/domains are created.</span></li>
<li><span class="legend me-2 bg-warning"></span><span>- there was a problem getting service status.</span></li>
</ul>
<p>
<!--p class="text-center text-secondary"><a href="/services/status">Edit Managed Services</a></p-->
</div>
<div id="service-table">
<table class="table table-striped">
<thead>
<tr>
<th>Service</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td>Loading;</td>
<td><span class="badge bg-secondary me-1"></span>Checking</td>
<td><a href="#" data-bs-toggle="tooltip" data-bs-placement="top" title="Restart service"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-rotate-clockwise-2" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M9 4.55a8 8 0 0 1 6 14.9m0 -4.45v5h5"></path><path d="M5.63 7.16l0 .01"></path><path d="M4.06 11l0 .01"></path><path d="M4.63 15.1l0 .01"></path><path d="M7.16 18.37l0 .01"></path><path d="M11 19.94l0 .01"></path></svg></a>
<a href="#" data-bs-toggle="tooltip" data-bs-placement="top" title="Stop OpenPanel service"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-player-stop" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 5m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z"></path></svg></a>
</td>
</tr>
<tr>
<td>Loading</td>
<td><span class="badge bg-secondary me-1"></span>Checking</td>
<td><a href="#" data-bs-toggle="tooltip" data-bs-placement="top" title="Restart service"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-rotate-clockwise-2" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M9 4.55a8 8 0 0 1 6 14.9m0 -4.45v5h5"></path><path d="M5.63 7.16l0 .01"></path><path d="M4.06 11l0 .01"></path><path d="M4.63 15.1l0 .01"></path><path d="M7.16 18.37l0 .01"></path><path d="M11 19.94l0 .01"></path></svg></a>
<a href="#" data-bs-toggle="tooltip" data-bs-placement="top" title="Stop OpenPanel service"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-player-stop" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 5m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z"></path></svg></a>
</td>
</tr>
<tr>
<td>Loading</td>
<td><span class="badge bg-secondary me-1"></span>Checking</td>
<td><a href="#" data-bs-toggle="tooltip" data-bs-placement="top" title="Restart service"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-rotate-clockwise-2" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M9 4.55a8 8 0 0 1 6 14.9m0 -4.45v5h5"></path><path d="M5.63 7.16l0 .01"></path><path d="M4.06 11l0 .01"></path><path d="M4.63 15.1l0 .01"></path><path d="M7.16 18.37l0 .01"></path><path d="M11 19.94l0 .01"></path></svg></a>
<a href="#" data-bs-toggle="tooltip" data-bs-placement="top" title="Stop OpenPanel service"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-player-stop" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 5m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z"></path></svg></a>
</td>
</tr>
<tr>
<td>Loading</td>
<td><span class="badge bg-secondary me-1"></span>Checking</td>
<td><a href="#" data-bs-toggle="tooltip" data-bs-placement="top" title="Restart service"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-rotate-clockwise-2" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M9 4.55a8 8 0 0 1 6 14.9m0 -4.45v5h5"></path><path d="M5.63 7.16l0 .01"></path><path d="M4.06 11l0 .01"></path><path d="M4.63 15.1l0 .01"></path><path d="M7.16 18.37l0 .01"></path><path d="M11 19.94l0 .01"></path></svg></a>
<a href="#" data-bs-toggle="tooltip" data-bs-placement="top" title="Stop OpenPanel service"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-player-stop" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 5m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z"></path></svg></a>
</td>
</tr>
<tr>
<td>Loading</td>
<td><span class="badge bg-secondary me-1"></span>Checking</td>
<td><a href="#" data-bs-toggle="tooltip" data-bs-placement="top" title="Restart service"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-rotate-clockwise-2" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M9 4.55a8 8 0 0 1 6 14.9m0 -4.45v5h5"></path><path d="M5.63 7.16l0 .01"></path><path d="M4.06 11l0 .01"></path><path d="M4.63 15.1l0 .01"></path><path d="M7.16 18.37l0 .01"></path><path d="M11 19.94l0 .01"></path></svg></a>
<a href="#" data-bs-toggle="tooltip" data-bs-placement="top" title="Stop OpenPanel service"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-player-stop" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 5m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z"></path></svg></a>
</td>
</tr>
<tr>
<td>Loading</td>
<td><span class="badge bg-secondary me-1"></span>Checking</td>
<td><a href="#" data-bs-toggle="tooltip" data-bs-placement="top" title="Restart service"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-rotate-clockwise-2" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M9 4.55a8 8 0 0 1 6 14.9m0 -4.45v5h5"></path><path d="M5.63 7.16l0 .01"></path><path d="M4.06 11l0 .01"></path><path d="M4.63 15.1l0 .01"></path><path d="M7.16 18.37l0 .01"></path><path d="M11 19.94l0 .01"></path></svg></a>
<a href="#" data-bs-toggle="tooltip" data-bs-placement="top" title="Stop OpenPanel service"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-player-stop" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 5m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z"></path></svg></a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-md-4 g-3">
<div class="card card-one" style="height:28rem">
<div class="card-header">
<div>
<h6 class="card-title">System Information</h6>
<p class="card-subtitle">Information about your server configuration.</p>
</div>
<nav class="nav nav-icon nav-icon-sm ms-auto"><a data-bs-toggle="collapse" class="nav-link collapsed" href="#system_info" role="button" aria-expanded="false" aria-controls="system_info"><i class="bi bi-question-circle"></i></a></nav>
</div>
<div class="card-body" style="padding:0">
<div class="collapse p-2" id="system_info" style="">
<p>Server hardware and OS information</p>
</div>
<div id="system-table">
<table class="table">
<tbody>
<tr>
<td>Hostname</td>
<td class="text-secondary">
<div class="placeholder col-12 mb-0"></div>
</td>
</tr>
<tr>
<td>OS</td>
<td class="text-secondary">
<div class="placeholder col-12 mb-0"></div>
</td>
</tr>
<tr>
<td>OpenPanel version</td>
<td class="text-secondary">
<div class="placeholder col-12 mb-0"></div>
</td>
</tr>
<tr>
<td>Server Time</td>
<td class="text-secondary">
<div class="placeholder col-12 mb-0" id="server-time"></div>
</td>
</tr>
<tr>
<td>Kernel</td>
<td class="text-secondary">
<div class="placeholder mb-0"></div>
</td>
</tr>
<tr>
<td>CPU</td>
<td class="text-secondary">
<div class="placeholder col-12 mb-0"></div>
</td>
</tr>
<tr>
<td>Uptime</td>
<td class="text-secondary">
<div class="placeholder col-12 mb-0"></div>
</td>
</tr>
<tr>
<td>Running Processes</td>
<td class="text-secondary">
<div class="placeholder col-12 mb-0"></div>
</td>
</tr>
<tr style="display:none;">
<td>Package Updates</td>
<td class="text-secondary">
<div class="placeholder col-12 mb-0"></div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
{% if license_type == "Community" %}
{% if try_enterprise %}
<!-- Include the try_enterprise.html template -->
{% include 'dashboard/try_enterprise.html' %}
{% endif %}
{% endif %}
{% if openpanel_news %}
<!-- Include the custom_message.html template -->
{% include 'dashboard/openpanel_news.html' %}
{% endif %}
{% if cpu_per_core_usage %}
<!-- Include the cpu_per_core_usage.html template -->
{% include 'dashboard/cpu_per_core_usage.html' %}
{% endif %}
</div>
<div class="col-12 mt-4">
<div class="card card-one">
<div class="card-header">
<div>
<h6 class="card-title">Disk Usage</h6>
<p class="card-subtitle">Current partition and hard disk usage.</p>
</div>
<nav class="nav nav-icon nav-icon-sm ms-auto"><a data-bs-toggle="collapse" class="nav-link collapsed" href="#disk_info" role="button" aria-expanded="false" aria-controls="disk_info"><i class="bi bi-question-circle"></i></a></nav>
</div>
<div class="card-body" style="padding:0;overflow:scroll">
<div class="collapse p-2" id="disk_info" style="">
<p>This section provides an overview of your system's disk usage. It displays information about each mounted disk partition, including details such as the device, mount point, filesystem type, and the amount of space used and available
in a human-readable format (in gigabytes, GB or terabytes, TB). The 'Usage Percentage' column indicates the percentage of disk space currently in use.</p>
<p>Partitions located within the<code>/home/</code>directory contain data inside users containers.</p>
</div>
<table id="disk-usage-table" class="table table-striped">
<thead>
<tr>
<th>Device</th>
<th>Type</th>
<th>Mountpoint</th>
<th>Used Space</th>
<th>Total Space</th>
<th>Free Space</th>
<th>%</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="placeholder col-9 mb-0"></div>
</td>
<td>
<div class="placeholder col-9 mb-0"></div>
</td>
<td>
<div class="progressbg">
<div class="progress progressbg-progress">
<div class="progress-bar bg-primary-lt" style="width:15.2%" role="progressbar" aria-valuenow="15.2" aria-valuemin="0" aria-valuemax="100" aria-label="15.2% Used"><span class="visually-hidden">15.2% Complete</span></div>
</div>
<div class="progressbg-text">/</div>
</div>
</td>
<td>
<div class="placeholder placeholder-xs col-8"></div>
</td>
<td>
<div class="placeholder placeholder-xs col-8"></div>
</td>
<td>
<div class="placeholder placeholder-xs col-8"></div>
</td>
<td class="">
<div class="placeholder placeholder-xs col-8"></div>
</td>
</tr>
<tr>
<td>
<div class="placeholder col-9 mb-0"></div>
</td>
<td>
<div class="placeholder col-9 mb-0"></div>
</td>
<td>
<div class="progressbg">
<div class="progress progressbg-progress">
<div class="progress-bar bg-primary-lt" style="width:55.2%" role="progressbar" aria-valuenow="55.2" aria-valuemin="0" aria-valuemax="100" aria-label="15.2% Used"><span class="visually-hidden">15.2% Complete</span></div>
</div>
<div class="progressbg-text">/</div>
</div>
</td>
<td>
<div class="placeholder placeholder-xs col-8"></div>
</td>
<td>
<div class="placeholder placeholder-xs col-8"></div>
</td>
<td>
<div class="placeholder placeholder-xs col-8"></div>
</td>
<td class="">
<div class="placeholder placeholder-xs col-8"></div>
</td>
</tr>
<tr>
<td>
<div class="placeholder col-9 mb-0"></div>
</td>
<td>
<div class="placeholder col-9 mb-0"></div>
</td>
<td>
<div class="progressbg">
<div class="progress progressbg-progress">
<div class="progress-bar bg-primary-lt" style="width:85.2%" role="progressbar" aria-valuenow="85.2" aria-valuemin="0" aria-valuemax="100" aria-label="15.2% Used"><span class="visually-hidden">15.2% Complete</span></div>
</div>
<div class="progressbg-text">/</div>
</div>
</td>
<td>
<div class="placeholder placeholder-xs col-8"></div>
</td>
<td>
<div class="placeholder placeholder-xs col-8"></div>
</td>
<td>
<div class="placeholder placeholder-xs col-8"></div>
</td>
<td class="">
<div class="placeholder placeholder-xs col-8"></div>
</td>
</tr>
<tr>
<td>
<div class="placeholder col-9 mb-0"></div>
</td>
<td>
<div class="placeholder col-9 mb-0"></div>
</td>
<td>
<div class="progressbg">
<div class="progress progressbg-progress">
<div class="progress-bar bg-primary-lt" style="width:85.2%" role="progressbar" aria-valuenow="85.2" aria-valuemin="0" aria-valuemax="100" aria-label="15.2% Used"><span class="visually-hidden">15.2% Complete</span></div>
</div>
<div class="progressbg-text">/</div>
</div>
</td>
<td>
<div class="placeholder placeholder-xs col-8"></div>
</td>
<td>
<div class="placeholder placeholder-xs col-8"></div>
</td>
<td>
<div class="placeholder placeholder-xs col-8"></div>
</td>
<td class="">
<div class="placeholder placeholder-xs col-8"></div>
</td>
</tr>
<tr>
<td>
<div class="placeholder col-9 mb-0"></div>
</td>
<td>
<div class="placeholder col-9 mb-0"></div>
</td>
<td>
<div class="progressbg">
<div class="progress progressbg-progress">
<div class="progress-bar bg-primary-lt" style="width:85.2%" role="progressbar" aria-valuenow="85.2" aria-valuemin="0" aria-valuemax="100" aria-label="15.2% Used"><span class="visually-hidden">15.2% Complete</span></div>
</div>
<div class="progressbg-text">/</div>
</div>
</td>
<td>
<div class="placeholder placeholder-xs col-8"></div>
</td>
<td>
<div class="placeholder placeholder-xs col-8"></div>
</td>
<td>
<div class="placeholder placeholder-xs col-8"></div>
</td>
<td class="">
<div class="placeholder placeholder-xs col-8"></div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<script src="{{ url_for('static', filename='pages/dashboard.js') }}" defer="defer"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
fetch('/json/system')
.then(response => response.json())
.then(data => {
let tableHtml = '<table class="table">';
// Hostname
tableHtml += `<tr><td>Hostname</td><td class="text-secondary">${data.hostname}</td></tr>`;
// OS
let osIcon = '';
if (data.os.toLowerCase().includes('ubuntu')) {
osIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-brand-ubuntu"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 5m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" /><path d="M17.723 7.41a7.992 7.992 0 0 0 -3.74 -2.162m-3.971 0a7.993 7.993 0 0 0 -3.789 2.216m-1.881 3.215a8 8 0 0 0 -.342 2.32c0 .738 .1 1.453 .287 2.132m1.96 3.428a7.993 7.993 0 0 0 3.759 2.19m4 0a7.993 7.993 0 0 0 3.747 -2.186m1.962 -3.43a8.008 8.008 0 0 0 .287 -2.131c0 -.764 -.107 -1.503 -.307 -2.203" /><path d="M5 17m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" /><path d="M19 17m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" /></svg>';
} else if (data.os.toLowerCase().includes('debian')) {
osIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-brand-debian"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M12 17c-2.397 -.943 -4 -3.153 -4 -5.635c0 -2.19 1.039 -3.14 1.604 -3.595c2.646 -2.133 6.396 -.27 6.396 3.23c0 2.5 -2.905 2.121 -3.5 1.5c-.595 -.621 -1 -1.5 -.5 -2.5" /><path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" /></svg>';
} else if (data.os.toLowerCase().includes('coreos')) {
osIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-brand-coreos"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M21 12a9 9 0 1 1 -18 0a9 9 0 0 1 18 0z" /><path d="M12 3c-3.263 3.212 -3 7.654 -3 12c4.59 .244 8.814 -.282 12 -3" /><path d="M9.5 9a4.494 4.494 0 0 1 5.5 5.5" /></svg>';
}
tableHtml += `<tr><td>OS</td><td class="text-secondary">${osIcon} ${data.os}</td></tr>`;
// OpenPanel version
tableHtml += `<tr><td>OpenPanel version</td><td class="text-secondary"><a href="https://openpanel.com/docs/changelog/${data.openpanel_version}" target="_blank">${data.openpanel_version}</a></td></tr>`;
// Time
tableHtml += `<tr><td>Server Time</td><td class="text-secondary" id="server-time">${data.time}</td></tr>`;
// Kernel
tableHtml += `<tr><td>Kernel</td><td class="text-secondary">${data.kernel}</td></tr>`;
// CPU with link
//tableHtml += `<tr><td>CPU</td><td class="text-secondary">${data.cpu}</td></tr>`;
let logoHtml = '';
if (data.cpu.includes('QEMU')) {
logoHtml = `<span data-bs-toggle="tooltip" data-bs-placement="right" title="${data.cpu}"><img src="{{ url_for('static', filename='images/dashboard/qemu.png') }}" style="height:2em;" alt="QEMU Logo"></span>`;
} else if (data.cpu.includes('Intel')) {
logoHtml = `<span data-bs-toggle="tooltip" data-bs-placement="right" title="${data.cpu}"><img src="{{ url_for('static', filename='images/dashboard/intel.png') }}" style="height:2em;" alt="Intel Logo"></span>`;
} else if (data.cpu.includes('AMD')) {
logoHtml = `<span data-bs-toggle="tooltip" data-bs-placement="right" title="${data.cpu}"><img src="{{ url_for('static', filename='images/dashboard/amd.svg') }}" style="height:2em;" alt="AMD Logo"></span>`;
} else if (data.cpu.includes('DO-')) {
logoHtml = `<span data-bs-toggle="tooltip" data-bs-placement="right" title="${data.cpu}"><img src="{{ url_for('static', filename='images/dashboard/do.png') }}" style="height:2em;" alt="DigitalOcean Logo"></span>`;
} else if (data.cpu.includes('AWS')) {
logoHtml = `<span data-bs-toggle="tooltip" data-bs-placement="right" title="${data.cpu}"><img src="{{ url_for('static', filename='images/dashboard/aws.png') }}" style="height:2em;" alt="AWS Logo"></span>`;
} else {
logoHtml = data.cpu; // for all others
}
tableHtml += `<tr><td>CPU</td><td class="text-secondary">${logoHtml}</td></tr>`;
// Uptime
let formattedUptime = formatUptime(data.uptime);
tableHtml += `<tr><td>Uptime</td><td class="text-secondary">${formattedUptime}</td></tr>`;
// Running Processes
tableHtml += `<tr><td>Running Processes</td><td class="text-secondary"><a href="/server/cpu_usage">${data.running_processes}</a></td></tr>`;
// Package updates
//tableHtml += `<tr><td>Package Updates</td><td class="text-secondary">${data.package_updates}</td></tr>`;
// Close the table tag
tableHtml += `</table>`;
document.getElementById('system-table').innerHTML = tableHtml;
// Start the fake time counter
startFakeTimeCounter(data.time);
})
.catch(error => {
console.error('Error fetching system info:', error);
document.getElementById('system-table').innerText = 'Failed to load system information.';
});
});
function formatUptime(seconds) {
const minute = 60,
hour = 3600,
day = 86400,
month = day * 30,
year = day * 365;
function pluralize(value, word) {
return `${value} ${word}${value > 1 ? 's' : ''}`;
}
if (seconds < minute) return pluralize(seconds, 'second');
else if (seconds < hour) return pluralize(Math.floor(seconds / minute), 'minute');
else if (seconds < day) return `${pluralize(Math.floor(seconds / hour), 'hour')}, ${pluralize(Math.floor((seconds % hour) / minute), 'minute')}`;
else if (seconds < month) return `${pluralize(Math.floor(seconds / day), 'day')}, ${pluralize(Math.floor((seconds % day) / hour), 'hour')}`;
else if (seconds < year) return `${pluralize(Math.floor(seconds / month), 'month')}, ${pluralize(Math.floor((seconds % month) / day), 'day')}`;
else return `${pluralize(Math.floor(seconds / year), 'year')}, ${pluralize(Math.floor((seconds % year) / month), 'month')}`;
}
function startFakeTimeCounter(serverTime) {
let timeElement = document.getElementById('server-time');
let currentTime = new Date(serverTime);
setInterval(() => {
currentTime.setSeconds(currentTime.getSeconds() + 1);
timeElement.textContent = currentTime.toISOString().split('T')[0] + ' ' + currentTime.toTimeString().split(' ')[0];
}, 1000);
}
</script>
<style>
.nav {
--tblr-nav-link-padding-x: 0;
--tblr-nav-link-padding-y: 0;
}
</style>
{% endblock %}

View File

@@ -0,0 +1,70 @@
<!-- Content of get_started.html -->
<style>
.dismiss-button {
display: none;
}
#get_started_content:hover .dismiss-button {
display: inline-block;
}
</style>
<div class="col-md-4 g-3">{% if force_domain and ns1 and ns2 and user_count > 0 and plan_count > 0 and is_modsec_installed == 'YES' and backup_jobs|int > 0 %} {% else %}
<div id="get_started_content" class="card card-one" style="min-height:28rem">
<div class="card-header">
<div>
<form action="{{ url_for('dismiss_dashboard_widget', template_name='get_started') }}" method="post">
<h6 class="card-title">Quick start guide <button data-bs-toggle="tooltip" data-bs-placement="top" aria-label="Hide and never show this quick start guide again" data-bs-original-title="Hide and never show this quick start guide again" class="dismiss-button btn btn-sm" type="submit">Dismiss</button></h6>
<p class="card-subtitle">Recommended steps for new installations.</p>
</form>
</div>
<div class="ribbon bg-dark">recommended</div>
</div>
<div class="card-body">{% if force_domain %}
<div class="alert alert-important alert-success" role="alert">
<div class="d-flex">
<div><svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 12l5 5l10 -10"></path></svg></div>
<div>Set custom domain instead of IP address</div>
</div>
</div>{% else %}
<a href="/settings/general">
<div class="alert alert-info" role="alert">Set custom domain instead of IP address</div>
</a>{% endif %} {% if ns1 and ns2 %}
<div class="alert alert-important alert-success" role="alert">
<div class="d-flex">
<div><svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 12l5 5l10 -10"></path></svg></div>
<div>Set custom nameservers to be used for domains</div>
</div>
</div>{% else %}
<a href="/settings/open-panel">
<div class="alert alert-info" role="alert">Set at least 2 nameservers</div>
</a>{% endif %} {% if user_count|int == 0 and plan_count|int == 0 %}
<a href="/users">
<div class="alert alert-info" role="alert">Create new Plan and create User account</div>
</a>{% elif user_count|int > 0 and plan_count|int > 0 %}
<div class="alert alert-important alert-success" role="alert">
<div class="d-flex">
<div><svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 12l5 5l10 -10"></path></svg></div>
<div>Create new Plan and create User account</div>
</div>
</div>{% elif plan_count|int > 0 and user_count|int == 0 %}
<a href="/users">
<div class="alert alert-info" role="alert">Create new Plan and create User account</div>
</a>{% elif plan_count|int == 0 and user_count|int > 0 %}
<a href="/plans">
<div class="alert alert-info" role="alert">Create new Plan and create User account</div>
</a>{% endif %}{% if backup_jobs|int > 0 and backup_jobs|int == 0 %}
<div class="alert alert-important alert-success" role="alert">
<div class="d-flex">
<div><svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 12l5 5l10 -10"></path></svg></div>
<div>Schedule remote Backups</div>
</div>
</div>{% else %}
<a href="/backups">
<div class="alert alert-info" role="alert">Schedule remote Backups</div>
</a>{% endif %}</div>
</div>{% endif %}
</div>

View File

@@ -0,0 +1,131 @@
<!-- Content of custom_message.html -->
<style>
.dismiss-custom_message-button {
display: none;
}
#custom_message:hover .dismiss-custom_message-button {
display: inline-block;
}
</style>
<div class="col-md-4 g-3">
<div id="custom_message" class="card card-one" style="height:28rem">
<div class="card-header">
<div>
<form action="{{ url_for('dismiss_dashboard_widget', template_name='openpanel_news') }}" method="post">
<h6 class="card-title">Latest News <button data-bs-toggle="tooltip" data-bs-placement="top" aria-label="Hide and never show news from openpanel.com/blog again" data-bs-original-title="Hide and never show news from openpanel.com/blog again" class="dismiss-custom_message-button btn btn-sm" type="submit">Dismiss</button></h6>
<p class="card-subtitle">from the openpanel.co blog</p>
</form>
</div>
<div class="ribbon bg-primary">news</div>
</div>
<div class="card-body card-body-scrollable card-body-scrollable-shadow">
<div class="divide-y" id="news-content">
</div>
</div>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", function() {
const newsContent = document.getElementById('news-content');
fetch('https://api.github.com/repos/stefanpejcic/OpenPanel/contents/website/blog')
.then(response => response.json())
.then(data => {
// Filter out non-markdown files and sort by date
const markdownFiles = data
.filter(file => file.name.endsWith('.md'))
.sort((a, b) => {
// Extract dates from file names (yyyy-mm-dd)
const dateA = new Date(a.name.split('-').slice(0, 3).join('-'));
const dateB = new Date(b.name.split('-').slice(0, 3).join('-'));
return dateB - dateA; // Sort descending (latest date first)
})
.slice(0, 5); // Get only the latest 5 files
// Function to fetch specific lines (title, description, slug, image)
const fetchRequiredLines = async (fileUrl) => {
const response = await fetch(fileUrl);
const text = await response.text();
const lines = text.split('\n');
const title = lines.find(line => line.startsWith('title:'))?.replace('title:', '').trim() || 'No Title';
const description = lines.find(line => line.startsWith('description:'))?.replace('description:', '').trim() || '';
const slug = lines.find(line => line.startsWith('slug:'))?.replace('slug:', '').trim() || '';
const image = lines.find(line => line.startsWith('image:'))?.replace('image:', '').trim() || '';
return { title, description, slug, image };
};
// Fetch and display file content
const fetchAndDisplayContent = async (file) => {
const fileUrl = `https://raw.githubusercontent.com/stefanpejcic/OpenPanel/main/website/blog/${file.name}`;
const { title, description, slug, image } = await fetchRequiredLines(fileUrl);
const newsItemDiv = document.createElement('div');
newsItemDiv.classList.add('row', 'align-items-center');
// Column for image (linked)
const imageDiv = document.createElement('div');
imageDiv.classList.add('col-3');
if (image) {
const imgLink = document.createElement('a');
imgLink.href = `https://openpanel.com/blog/${slug}`;
imgLink.target = '_blank';
const img = document.createElement('img');
img.src = image;
img.alt = title;
img.classList.add('rounded');
img.style.maxWidth = '100%';
imgLink.appendChild(img);
imageDiv.appendChild(imgLink);
}
// Column for title and description
const contentDiv = document.createElement('div');
contentDiv.classList.add('col');
const titleElement = document.createElement('h3');
titleElement.classList.add('card-title', 'mb-1');
const titleLink = document.createElement('a');
titleLink.href = `https://openpanel.com/blog/${slug}`;
titleLink.target = '_blank';
titleLink.classList.add('text-reset');
titleLink.textContent = title;
titleElement.appendChild(titleLink);
const descriptionElement = document.createElement('div');
descriptionElement.classList.add('text-secondary');
descriptionElement.textContent = description;
contentDiv.appendChild(titleElement);
contentDiv.appendChild(descriptionElement);
// Append image and content to newsItemDiv
newsItemDiv.appendChild(imageDiv);
newsItemDiv.appendChild(contentDiv);
// Append newsItemDiv to newsContent
newsContent.appendChild(newsItemDiv);
};
// Loop through each markdown file and fetch/display content
markdownFiles.forEach(file => {
fetchAndDisplayContent(file);
});
})
.catch(error => {
console.error('Error fetching the GitHub API:', error);
newsContent.innerHTML = '<p>Error loading news</p>';
});
});
</script>

View File

@@ -0,0 +1,92 @@
<!-- Content of try_enterprise.html -->
<style>
.dismiss-custom_message-button {
display: none;
}
#custom_message:hover .dismiss-custom_message-button {
display: inline-block;
}
</style>
<div class="col-md-4 g-3">
<div id="custom_message" class="card card-one" style="height:28rem">
<div class="card-header">
<div class="col">
<form action="{{ url_for('dismiss_dashboard_widget', template_name='try_enterprise') }}" method="post">
<h6 class="card-title">Try Enterprise <button data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="Hide and never show option to upgrade to Enterprise again" class="dismiss-custom_message-button btn btn-sm" type="submit">Dismiss</button></h6>
<p class="card-subtitle" ><span id="pricing-container"></span></p>
</form>
</div>
<div class="col-auto">
<a href="/license" id="purchaselink" class="btn btn-dark">Upgrade Now</a>
</div>
</div>
<div class="card-body">
<p class="text-secondary"><b>OpenPanel Enterprise edition</b> offers advanced features for user isolation and management, suitable for web hosting providers.</p>
<div class="divide-y-2 mt-4" id="features-container"></div>
</div>
<script>
$(document).ready(function() {
$.ajax({
url: 'https://api.openpanel.com/whmcs.php',
method: 'GET',
dataType: 'json',
success: function(response) {
if (response.result === 'success' && response.totalresults > 0) {
const product = response.products.product[0]; // Get the first product
const pricing = product.pricing.EUR; // Get the pricing in EUR
// Display price information
const pricingHtml = `Starting at ${pricing.monthly} ${pricing.suffix} monthly`;
$('#pricing-container').html(pricingHtml);
// Update the purchase link
const productUrl = product.product_url; // Get the product URL
$('#purchaselink').attr('href', productUrl); // Set the href attribute
// Parse the features from the description
const featureListMatch = product.description.match(/<ul>(.*?)<\/ul>/s);
if (featureListMatch) {
const features = featureListMatch[1].match(/<li>(.*?)<\/li>/g).map(item => item.replace(/<li>|<\/li>/g, '').trim());
const featuresContainer = $('#features-container');
featuresContainer.empty(); // Clear existing features
features.forEach(feature => {
const featureHtml = `
<div>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon text-green">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M5 12l5 5l10 -10"></path>
</svg>
${feature}
</div>
`;
featuresContainer.append(featureHtml);
});
} else {
$('#features-container').html('<p>No features found.</p>');
}
} else {
$('#pricing-container').html('<p>No products found.</p>');
}
},
error: function(xhr, status, error) {
console.error('AJAX Error:', status, error);
console.error('Response:', xhr.responseText);
$('#pricing-container').html('<p>Error retrieving pricing information.</p>');
}
});
});
</script>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More