mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-02-19 21:16:49 +01:00
Merge origin/v2.5.5-dev: Plugin Store sidebar, ?view=store, master3395 default, clone comment
This commit is contained in:
33
.github/scripts/ci-validate-upgrade.sh
vendored
Executable file
33
.github/scripts/ci-validate-upgrade.sh
vendored
Executable file
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run inside CI Docker container (or repo root). Validates upgrade loader + modules and key files.
|
||||
set -e
|
||||
|
||||
echo "=== Shell syntax check ==="
|
||||
for f in cyberpanel_upgrade.sh preUpgrade.sh fix-phpmyadmin.sh cyberpanel.sh cyberpanel_utility.sh; do
|
||||
[ ! -f "$f" ] && continue
|
||||
bash -n "$f" || { echo "FAIL (syntax): $f"; exit 1; }
|
||||
echo "OK $f"
|
||||
done
|
||||
test -f preUpgrade.sh && test -f cyberpanel_upgrade.sh || { echo "Missing required scripts"; exit 1; }
|
||||
|
||||
echo "=== Key files ==="
|
||||
for f in preUpgrade.sh cyberpanel_upgrade.sh plogical/upgrade.py install/install.py; do
|
||||
test -f "$f" || { echo "Missing: $f"; exit 1; }
|
||||
done
|
||||
grep -q 'BRANCH_NAME' preUpgrade.sh || exit 1
|
||||
|
||||
if [ -d upgrade_modules ]; then
|
||||
for n in 00_common 01_variables 02_checks 03_mariadb 04_git_url 05_repository 06_components 07_branch_input 08_main_upgrade 09_sync 10_post_tweak 11_display_final; do
|
||||
test -f "upgrade_modules/${n}.sh" || { echo "Missing: upgrade_modules/${n}.sh"; exit 1; }
|
||||
done
|
||||
grep -q 'Branch_Check\|Branch_Name' upgrade_modules/00_common.sh upgrade_modules/02_checks.sh || exit 1
|
||||
grep -q 'upgrade_modules\|Pre_Upgrade_Branch_Input\|Set_Default_Variables' cyberpanel_upgrade.sh || exit 1
|
||||
for f in upgrade_modules/*.sh; do
|
||||
[ -f "$f" ] || continue
|
||||
bash -n "$f" || { echo "FAIL (syntax): $f"; exit 1; }
|
||||
done
|
||||
else
|
||||
grep -q 'Branch_Name\|download_install_phpmyadmin\|Branch_Check' cyberpanel_upgrade.sh || exit 1
|
||||
fi
|
||||
|
||||
echo "Done."
|
||||
99
.github/workflows/ci.yml
vendored
Normal file
99
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
# Lightweight CI for v2.5.5-dev (free for public repos).
|
||||
# Validates shell syntax, Python version fetcher, key files, and upgrade script.
|
||||
# Pinned to ubuntu-22.04. Multi-OS validation: run locally with
|
||||
# for img in almalinux:8 centos:7 debian:12 ubuntu:24.04; do docker run --rm -v "$PWD:/repo:ro" -w /repo $img bash /repo/.github/scripts/ci-validate-upgrade.sh; done
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [v2.5.5-dev, stable, master]
|
||||
pull_request:
|
||||
branches: [v2.5.5-dev, stable, master]
|
||||
|
||||
jobs:
|
||||
validate-shell:
|
||||
name: Validate shell scripts
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Syntax check shell scripts
|
||||
run: |
|
||||
for f in cyberpanel_upgrade.sh preUpgrade.sh fix-phpmyadmin.sh cyberpanel.sh cyberpanel_utility.sh; do
|
||||
[ ! -f "$f" ] && continue
|
||||
bash -n "$f" || { echo "FAIL (syntax): $f"; exit 1; }
|
||||
echo "OK $f"
|
||||
done
|
||||
test -f preUpgrade.sh && test -f cyberpanel_upgrade.sh || { echo "Missing required scripts"; exit 1; }
|
||||
if [ -d upgrade_modules ]; then
|
||||
echo "=== Upgrade modules syntax ==="
|
||||
for f in upgrade_modules/*.sh; do
|
||||
[ -f "$f" ] || continue
|
||||
bash -n "$f" || { echo "FAIL (syntax): $f"; exit 1; }
|
||||
echo "OK $f"
|
||||
done
|
||||
fi
|
||||
echo "All shell scripts passed syntax check"
|
||||
|
||||
validate-python:
|
||||
name: Validate Python (version fetcher)
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 2
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: Install requests
|
||||
run: pip install requests
|
||||
- name: Run version fetcher (phpMyAdmin / SnappyMail)
|
||||
run: |
|
||||
if [ ! -f plogical/versionFetcher.py ]; then
|
||||
echo "Skipping (plogical/versionFetcher.py not present on this branch)"
|
||||
exit 0
|
||||
fi
|
||||
PYTHONPATH=. python3 -c "
|
||||
from plogical.versionFetcher import get_latest_phpmyadmin_version, get_latest_snappymail_version
|
||||
pma = get_latest_phpmyadmin_version()
|
||||
snappy = get_latest_snappymail_version()
|
||||
assert pma and len(pma) >= 5, 'phpMyAdmin version invalid'
|
||||
assert snappy and len(snappy) >= 3, 'SnappyMail version invalid'
|
||||
print('phpMyAdmin:', pma, 'SnappyMail:', snappy)
|
||||
"
|
||||
|
||||
smoke-key-files:
|
||||
name: Key files present
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check key install/upgrade files exist
|
||||
run: |
|
||||
for f in preUpgrade.sh cyberpanel_upgrade.sh plogical/upgrade.py install/install.py; do
|
||||
test -f "$f" || { echo "Missing: $f"; exit 1; }
|
||||
done
|
||||
test -f plogical/versionFetcher.py && echo "versionFetcher.py present" || echo "versionFetcher.py optional (not on all branches)"
|
||||
grep -q 'BRANCH_NAME' preUpgrade.sh || exit 1
|
||||
if [ -d upgrade_modules ]; then
|
||||
for n in 00_common 01_variables 02_checks 03_mariadb 04_git_url 05_repository 06_components 07_branch_input 08_main_upgrade 09_sync 10_post_tweak 11_display_final; do
|
||||
test -f "upgrade_modules/${n}.sh" || { echo "Missing: upgrade_modules/${n}.sh"; exit 1; }
|
||||
done
|
||||
grep -q 'Branch_Check\|Branch_Name' upgrade_modules/00_common.sh upgrade_modules/02_checks.sh || exit 1
|
||||
grep -q 'upgrade_modules\|Pre_Upgrade_Branch_Input\|Set_Default_Variables' cyberpanel_upgrade.sh || exit 1
|
||||
else
|
||||
grep -q 'Branch_Name\|download_install_phpmyadmin\|Branch_Check' cyberpanel_upgrade.sh || exit 1
|
||||
fi
|
||||
echo "Key files OK"
|
||||
|
||||
# Run upgrade validation script (same checks as other jobs; no Docker to avoid runner issues).
|
||||
validate-upgrade-script:
|
||||
name: Validate upgrade script
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run ci-validate-upgrade.sh
|
||||
run: |
|
||||
set -e
|
||||
if [ ! -f .github/scripts/ci-validate-upgrade.sh ]; then
|
||||
echo "Missing .github/scripts/ci-validate-upgrade.sh"
|
||||
exit 1
|
||||
fi
|
||||
bash .github/scripts/ci-validate-upgrade.sh
|
||||
@@ -55,13 +55,13 @@ elif grep -q -E "CloudLinux 7|CloudLinux 8" /etc/os-release ; then
|
||||
Server_OS="CloudLinux"
|
||||
elif grep -q -E "Rocky Linux" /etc/os-release ; then
|
||||
Server_OS="RockyLinux"
|
||||
elif grep -q -E "Ubuntu 18.04|Ubuntu 20.04|Ubuntu 20.10|Ubuntu 22.04" /etc/os-release ; then
|
||||
elif grep -q -E "Ubuntu 18.04|Ubuntu 20.04|Ubuntu 20.10|Ubuntu 22.04|Ubuntu 24.04" /etc/os-release ; then
|
||||
Server_OS="Ubuntu"
|
||||
elif grep -q -E "openEuler 20.03|openEuler 22.03" /etc/os-release ; then
|
||||
Server_OS="openEuler"
|
||||
else
|
||||
echo -e "Unable to detect your system..."
|
||||
echo -e "\nCyberPanel is supported on x86_64 based Ubuntu 18.04, Ubuntu 20.04, Ubuntu 20.10, Ubuntu 22.04, CentOS 7, CentOS 8, AlmaLinux 8, RockyLinux 8, CloudLinux 7, CloudLinux 8, openEuler 20.03, openEuler 22.03...\n"
|
||||
echo -e "\nCyberPanel is supported on x86_64 based Ubuntu 18.04, Ubuntu 20.04, Ubuntu 20.10, Ubuntu 22.04, Ubuntu 24.04, CentOS 7, CentOS 8, AlmaLinux 8, AlmaLinux 9, AlmaLinux 10, RockyLinux 8, CloudLinux 7, CloudLinux 8, openEuler 20.03, openEuler 22.03...\n"
|
||||
exit
|
||||
fi
|
||||
|
||||
|
||||
@@ -12,13 +12,13 @@ elif grep -q -E "CloudLinux 7|CloudLinux 8" /etc/os-release ; then
|
||||
Server_OS="CloudLinux"
|
||||
elif grep -q -E "Rocky Linux" /etc/os-release ; then
|
||||
Server_OS="RockyLinux"
|
||||
elif grep -q -E "Ubuntu 18.04|Ubuntu 20.04|Ubuntu 20.10|Ubuntu 22.04" /etc/os-release ; then
|
||||
elif grep -q -E "Ubuntu 18.04|Ubuntu 20.04|Ubuntu 20.10|Ubuntu 22.04|Ubuntu 24.04" /etc/os-release ; then
|
||||
Server_OS="Ubuntu"
|
||||
elif grep -q -E "openEuler 20.03|openEuler 22.03" /etc/os-release ; then
|
||||
Server_OS="openEuler"
|
||||
else
|
||||
echo -e "Unable to detect your system..."
|
||||
echo -e "\nCyberPanel is supported on x86_64 based Ubuntu 18.04, Ubuntu 20.04, Ubuntu 20.10, Ubuntu 22.04, CentOS 7, CentOS 8, AlmaLinux 8, RockyLinux 8, CloudLinux 7, CloudLinux 8, openEuler 20.03, openEuler 22.03...\n"
|
||||
echo -e "\nCyberPanel is supported on x86_64 based Ubuntu 18.04, Ubuntu 20.04, Ubuntu 20.10, Ubuntu 22.04, Ubuntu 24.04, CentOS 7, CentOS 8, AlmaLinux 8, AlmaLinux 9, AlmaLinux 10, RockyLinux 8, CloudLinux 7, CloudLinux 8, openEuler 20.03, openEuler 22.03...\n"
|
||||
exit
|
||||
fi
|
||||
|
||||
|
||||
89
CPScripts/migrate_rainloop_to_snappymail.sh
Normal file
89
CPScripts/migrate_rainloop_to_snappymail.sh
Normal file
@@ -0,0 +1,89 @@
|
||||
#!/bin/bash
|
||||
# Migrate RainLoop to SnappyMail on live server: rename public folder and/or install SnappyMail if missing, migrate data, fix paths, restart, test.
|
||||
# Run as root: bash /usr/local/CyberCP/CPScripts/migrate_rainloop_to_snappymail.sh
|
||||
set -e
|
||||
LOG="/var/log/cyberpanel_upgrade_debug.log"
|
||||
log() { echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] $*" | tee -a "$LOG"; }
|
||||
SNAPPY_VERSION="${SNAPPY_VERSION:-2.38.2}"
|
||||
|
||||
log "=== RainLoop -> SnappyMail migration ==="
|
||||
|
||||
# 1) Ensure public/snappymail exists: rename rainloop -> snappymail OR download and install SnappyMail
|
||||
if [ -d "/usr/local/CyberCP/public/rainloop" ] && [ ! -d "/usr/local/CyberCP/public/snappymail" ]; then
|
||||
log "Renaming public/rainloop to public/snappymail..."
|
||||
mv /usr/local/CyberCP/public/rainloop /usr/local/CyberCP/public/snappymail
|
||||
log "Renamed public/rainloop -> public/snappymail"
|
||||
if [ -f "/usr/local/CyberCP/public/snappymail/include.php" ]; then
|
||||
sed -i 's|/usr/local/lscp/cyberpanel/rainloop/data|/usr/local/lscp/cyberpanel/snappymail/data|g' /usr/local/CyberCP/public/snappymail/include.php
|
||||
log "Updated include.php data path"
|
||||
fi
|
||||
for inc in /usr/local/CyberCP/public/snappymail/snappymail/v/*/include.php /usr/local/CyberCP/public/snappymail/rainloop/v/*/include.php; do
|
||||
[ -f "$inc" ] && sed -i 's|/usr/local/lscp/cyberpanel/rainloop/data|/usr/local/lscp/cyberpanel/snappymail/data|g' "$inc" && log "Updated $inc" && break
|
||||
done 2>/dev/null || true
|
||||
elif [ ! -d "/usr/local/CyberCP/public/snappymail" ]; then
|
||||
log "public/snappymail missing - installing SnappyMail v${SNAPPY_VERSION}..."
|
||||
cd /usr/local/CyberCP/public || exit 1
|
||||
if ! wget -q "https://github.com/the-djmaze/snappymail/releases/download/v${SNAPPY_VERSION}/snappymail-${SNAPPY_VERSION}.zip" -O "snappymail-${SNAPPY_VERSION}.zip"; then
|
||||
log "ERROR: wget SnappyMail failed"
|
||||
exit 1
|
||||
fi
|
||||
unzip -q "snappymail-${SNAPPY_VERSION}.zip" -d /usr/local/CyberCP/public/snappymail
|
||||
rm -f "snappymail-${SNAPPY_VERSION}.zip"
|
||||
find /usr/local/CyberCP/public/snappymail -type d -exec chmod 755 {} \;
|
||||
find /usr/local/CyberCP/public/snappymail -type f -exec chmod 644 {} \;
|
||||
log "SnappyMail installed"
|
||||
# Configure data path and run CyberPanel integration
|
||||
wget -q -O /usr/local/CyberCP/snappymail_cyberpanel.php "https://raw.githubusercontent.com/the-djmaze/snappymail/master/integrations/cyberpanel/install.php" 2>/dev/null || true
|
||||
if [ -f /usr/local/CyberCP/snappymail_cyberpanel.php ]; then
|
||||
for php in /usr/local/lsws/lsphp83/bin/php /usr/local/lsws/lsphp82/bin/php /usr/local/lsws/lsphp81/bin/php /usr/local/lsws/lsphp80/bin/php; do
|
||||
[ -x "$php" ] && $php /usr/local/CyberCP/snappymail_cyberpanel.php 2>/dev/null && break
|
||||
done
|
||||
fi
|
||||
else
|
||||
log "public/snappymail already exists"
|
||||
fi
|
||||
|
||||
# 2) Data migration: lscp/cyberpanel/rainloop/data -> snappymail/data
|
||||
mkdir -p /usr/local/lscp/cyberpanel/snappymail/data/_data_/_default_/configs/
|
||||
mkdir -p /usr/local/lscp/cyberpanel/snappymail/data/_data_/_default_/domains/
|
||||
mkdir -p /usr/local/lscp/cyberpanel/snappymail/data/_data_/_default_/storage/
|
||||
mkdir -p /usr/local/lscp/cyberpanel/snappymail/data/_data_/_default_/temp/
|
||||
mkdir -p /usr/local/lscp/cyberpanel/snappymail/data/_data_/_default_/cache/
|
||||
|
||||
if [ -d "/usr/local/lscp/cyberpanel/rainloop/data" ] && [ "$(ls -A /usr/local/lscp/cyberpanel/rainloop/data 2>/dev/null)" ]; then
|
||||
log "Migrating rainloop data to snappymail..."
|
||||
rsync -av --ignore-existing /usr/local/lscp/cyberpanel/rainloop/data/ /usr/local/lscp/cyberpanel/snappymail/data/ 2>&1 | tee -a "$LOG"
|
||||
if [ -f "/usr/local/CyberCP/public/snappymail/include.php" ]; then
|
||||
sed -i 's|/usr/local/lscp/cyberpanel/rainloop/data|/usr/local/lscp/cyberpanel/snappymail/data|g' /usr/local/CyberCP/public/snappymail/include.php
|
||||
fi
|
||||
find /usr/local/lscp/cyberpanel/snappymail/data -type f \( -name "*.ini" -o -name "*.json" -o -name "*.php" -o -name "*.cfg" \) -exec grep -l "rainloop" {} \; 2>/dev/null | while read -r f; do
|
||||
sed -i 's|/usr/local/lscp/cyberpanel/rainloop/data|/usr/local/lscp/cyberpanel/snappymail/data|g; s|/rainloop/|/snappymail/|g; s|rainloop/data|snappymail/data|g' "$f"
|
||||
done
|
||||
log "Data migration done"
|
||||
fi
|
||||
|
||||
# 3) Ownership and permissions
|
||||
if id -u lscpd >/dev/null 2>&1; then
|
||||
chown -R lscpd:lscpd /usr/local/lscp/cyberpanel/snappymail/
|
||||
chown -R lscpd:lscpd /usr/local/CyberCP/public/snappymail/ 2>/dev/null || true
|
||||
log "Set ownership lscpd:lscpd"
|
||||
fi
|
||||
chmod -R 775 /usr/local/lscp/cyberpanel/snappymail/data/ 2>/dev/null || true
|
||||
|
||||
# 4) Restart panel
|
||||
log "Restarting lscpd..."
|
||||
systemctl restart lscpd
|
||||
sleep 2
|
||||
|
||||
# 5) Test
|
||||
PORT=$(cat /usr/local/lscp/conf/bind.conf 2>/dev/null | grep -oE '[0-9]+' | head -1)
|
||||
PORT=${PORT:-8090}
|
||||
log "Testing https://127.0.0.1:${PORT}/snappymail/index.php ..."
|
||||
CODE=$(curl -sk -o /dev/null -w "%{http_code}" "https://127.0.0.1:${PORT}/snappymail/index.php" 2>/dev/null || echo "000")
|
||||
if [ "$CODE" = "200" ] || [ "$CODE" = "302" ]; then
|
||||
log "OK: SnappyMail URL returned HTTP $CODE"
|
||||
else
|
||||
log "WARNING: SnappyMail URL returned HTTP $CODE (expected 200 or 302)"
|
||||
fi
|
||||
|
||||
log "=== Migration script finished ==="
|
||||
@@ -267,7 +267,7 @@ class secMiddleware:
|
||||
|
||||
response['X-XSS-Protection'] = "1; mode=block"
|
||||
response['X-Frame-Options'] = "sameorigin"
|
||||
response['Content-Security-Policy'] = "script-src 'self' https://www.jsdelivr.com"
|
||||
response['Content-Security-Policy'] = "script-src 'self' 'unsafe-inline' https://www.jsdelivr.com"
|
||||
response['Content-Security-Policy'] = "connect-src *;"
|
||||
response['Content-Security-Policy'] = "font-src 'self' 'unsafe-inline' https://www.jsdelivr.com https://fonts.googleapis.com"
|
||||
response[
|
||||
|
||||
@@ -13,15 +13,15 @@ https://docs.djangoproject.com/en/1.11/ref/settings/
|
||||
import os
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
# Patreon OAuth Configuration for Paid Plugins
|
||||
# SECURITY: Environment variables take precedence. Hardcoded values are fallback for this server only.
|
||||
# For repository version, use empty defaults and set via environment variables.
|
||||
PATREON_CLIENT_ID = os.environ.get('PATREON_CLIENT_ID', 'LFXeXUcfrM8MeVbUcmGbB7BgeJ9RzZi2v_H9wL4d9vG6t1dV4SUnQ4ibn9IYzvt7')
|
||||
PATREON_CLIENT_SECRET = os.environ.get('PATREON_CLIENT_SECRET', 'APuJ5qoL3TLFmNnGDVkgl-qr3sCzp2CQsKfslBbp32hhnhlD0y6-ZcSCkb_FaUJv')
|
||||
# Patreon OAuth (optional): for paid-plugin verification via Patreon membership.
|
||||
# Set these only if you use Patreon-gated plugins; leave unset otherwise.
|
||||
# Use environment variables; no defaults so the repo stays generic and safe to push to GitHub.
|
||||
PATREON_CLIENT_ID = os.environ.get('PATREON_CLIENT_ID', '')
|
||||
PATREON_CLIENT_SECRET = os.environ.get('PATREON_CLIENT_SECRET', '')
|
||||
PATREON_CREATOR_ID = os.environ.get('PATREON_CREATOR_ID', '')
|
||||
PATREON_MEMBERSHIP_TIER_ID = os.environ.get('PATREON_MEMBERSHIP_TIER_ID', '27789984') # CyberPanel Paid Plugin tier
|
||||
PATREON_CREATOR_ACCESS_TOKEN = os.environ.get('PATREON_CREATOR_ACCESS_TOKEN', 'niAHRiI9SgrRCMmaf5exoXXphy3RWXWsX4kO5Yv9SQI')
|
||||
PATREON_CREATOR_REFRESH_TOKEN = os.environ.get('PATREON_CREATOR_REFRESH_TOKEN', 'VZlCQoPwJUr4NLni1N82-K_CpJHTAOYUOCx2PujdjQg')
|
||||
PATREON_MEMBERSHIP_TIER_ID = os.environ.get('PATREON_MEMBERSHIP_TIER_ID', '')
|
||||
PATREON_CREATOR_ACCESS_TOKEN = os.environ.get('PATREON_CREATOR_ACCESS_TOKEN', '')
|
||||
PATREON_CREATOR_REFRESH_TOKEN = os.environ.get('PATREON_CREATOR_REFRESH_TOKEN', '')
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
@@ -37,6 +37,22 @@ DEBUG = False
|
||||
|
||||
ALLOWED_HOSTS = ['*']
|
||||
|
||||
# When the panel is behind a reverse proxy (e.g. https://panel.example.com -> http://backend:port),
|
||||
# the browser sends Origin/Referer with the public domain while the proxy may send Host as the
|
||||
# backend address. Django then fails CSRF (Referer vs Host mismatch) and POSTs get 403.
|
||||
# Set CSRF_TRUSTED_ORIGINS to your public origin(s) so CSRF passes. Optional; leave unset if
|
||||
# you access the panel by IP:port only.
|
||||
# Example: export CSRF_TRUSTED_ORIGINS="https://panel.example.com,http://panel.example.com"
|
||||
_csrf_origins_env = os.environ.get('CSRF_TRUSTED_ORIGINS', '')
|
||||
_csrf_origins_list = [o.strip() for o in _csrf_origins_env.split(',') if o.strip()]
|
||||
# Add default trusted origins for common CyberPanel domains
|
||||
_default_origins = [
|
||||
'https://cyberpanel.newstargeted.com',
|
||||
'http://cyberpanel.newstargeted.com',
|
||||
]
|
||||
# Merge environment and default origins, avoiding duplicates
|
||||
CSRF_TRUSTED_ORIGINS = list(dict.fromkeys(_csrf_origins_list + _default_origins))
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
@@ -65,7 +81,8 @@ INSTALLED_APPS = [
|
||||
|
||||
# Apps with multiple or complex dependencies
|
||||
'emailPremium',
|
||||
'emailMarketing', # Depends on websiteFunctions and loginSystem
|
||||
# Optional plugins (e.g. emailMarketing, discordWebhooks) - install via Plugin Store
|
||||
# from https://github.com/master3395/cyberpanel-plugins - plugin installer adds them
|
||||
'cloudAPI', # Depends on websiteFunctions
|
||||
'containerization', # Depends on websiteFunctions
|
||||
'IncBackups', # Depends on websiteFunctions and loginSystem
|
||||
@@ -123,6 +140,8 @@ TEMPLATES = [
|
||||
'baseTemplate.context_processors.version_context',
|
||||
'baseTemplate.context_processors.cosmetic_context',
|
||||
'baseTemplate.context_processors.notification_preferences_context',
|
||||
'baseTemplate.context_processors.firewall_static_context',
|
||||
'baseTemplate.context_processors.dns_static_context',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -192,6 +211,9 @@ STATIC_ROOT = os.path.join(BASE_DIR, "static/")
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
# Panel public directory (SnappyMail, phpMyAdmin, etc.) – served so /snappymail/ and /phpmyadmin/ work when panel is behind Django
|
||||
PUBLIC_ROOT = os.path.join(BASE_DIR, 'public')
|
||||
|
||||
LOCALE_PATHS = (
|
||||
os.path.join(BASE_DIR, 'locale'),
|
||||
)
|
||||
@@ -230,4 +252,25 @@ LOGIN_REDIRECT_URL = '/'
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
# Sync INSTALLED_APPS with plugins on disk so /plugins/<name>/ and /plugins/<name>/settings/ work.
|
||||
# Plugins installed under /usr/local/CyberCP/ (or BASE_DIR) are added here if they have meta.xml + urls.py.
|
||||
_cybercp_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
if os.path.isdir(_cybercp_root):
|
||||
try:
|
||||
_existing_apps = set(INSTALLED_APPS)
|
||||
for _name in os.listdir(_cybercp_root):
|
||||
if _name.startswith('.'):
|
||||
continue
|
||||
_plugin_dir = os.path.join(_cybercp_root, _name)
|
||||
if not os.path.isdir(_plugin_dir):
|
||||
continue
|
||||
if _name in _existing_apps:
|
||||
continue
|
||||
if (os.path.exists(os.path.join(_plugin_dir, 'meta.xml')) and
|
||||
os.path.exists(os.path.join(_plugin_dir, 'urls.py'))):
|
||||
INSTALLED_APPS.append(_name)
|
||||
_existing_apps.add(_name)
|
||||
except (OSError, IOError):
|
||||
pass
|
||||
@@ -13,16 +13,34 @@ Including another URLconf
|
||||
1. Import the include() function: from django.urls import path, include
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
import os
|
||||
from django.urls import path, re_path, include
|
||||
from django.contrib import admin
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.views.static import serve
|
||||
from django.views.generic import RedirectView
|
||||
from firewall import views as firewall_views
|
||||
|
||||
# Plugin routes are no longer hardcoded here; pluginHolder.urls dynamically
|
||||
# includes each installed plugin (under /plugins/<name>/) so settings and
|
||||
# other plugin pages work for any installed plugin.
|
||||
|
||||
# Optional app: may be missing after clean clone or git clean -fd (not in all repo trees)
|
||||
_optional_email_marketing = []
|
||||
try:
|
||||
_optional_email_marketing.append(path('emailMarketing/', include('emailMarketing.urls')))
|
||||
except ModuleNotFoundError:
|
||||
pass
|
||||
|
||||
urlpatterns = [
|
||||
# Serve static files first (before catch-all routes)
|
||||
re_path(r'^static/(?P<path>.*)$', serve, {'document_root': settings.STATIC_ROOT}),
|
||||
# Serve SnappyMail and phpMyAdmin from public directory (fixes 404 when panel is served by Django/lscpd on :2087/:8090)
|
||||
re_path(r'^snappymail/?$', RedirectView.as_view(url='/snappymail/index.php', permanent=False)),
|
||||
re_path(r'^snappymail/(?P<path>.*)$', serve, {'document_root': os.path.join(settings.PUBLIC_ROOT, 'snappymail')}),
|
||||
re_path(r'^phpmyadmin/?$', RedirectView.as_view(url='/phpmyadmin/index.php', permanent=False)),
|
||||
re_path(r'^phpmyadmin/(?P<path>.*)$', serve, {'document_root': os.path.join(settings.PUBLIC_ROOT, 'phpmyadmin')}),
|
||||
path('base/', include('baseTemplate.urls')),
|
||||
path('imunifyav/', firewall_views.imunifyAV, name='imunifyav_root'),
|
||||
path('ImunifyAV/', firewall_views.imunifyAV, name='imunifyav_root_legacy'),
|
||||
@@ -43,6 +61,7 @@ urlpatterns = [
|
||||
path('api/', include('api.urls')),
|
||||
path('filemanager/', include('filemanager.urls')),
|
||||
path('emailPremium/', include('emailPremium.urls')),
|
||||
*_optional_email_marketing,
|
||||
path('manageservices/', include('manageServices.urls')),
|
||||
path('plugins/', include('pluginHolder.urls')),
|
||||
path('cloudAPI/', include('cloudAPI.urls')),
|
||||
@@ -51,7 +70,6 @@ urlpatterns = [
|
||||
path('CloudLinux/', include('CLManager.urls')),
|
||||
path('IncrementalBackups/', include('IncBackups.urls')),
|
||||
path('aiscanner/', include('aiScanner.urls')),
|
||||
path('emailMarketing/', include('emailMarketing.urls')),
|
||||
# path('Terminal/', include('WebTerminal.urls')),
|
||||
path('', include('loginSystem.urls')),
|
||||
]
|
||||
|
||||
@@ -8,7 +8,13 @@ https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Ensure CyberPanel app path takes precedence over system 'firewall' package
|
||||
PROJECT_ROOT = '/usr/local/CyberCP'
|
||||
while PROJECT_ROOT in sys.path:
|
||||
sys.path.remove(PROJECT_ROOT)
|
||||
sys.path.insert(0, PROJECT_ROOT)
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
|
||||
69
README.md
69
README.md
@@ -7,7 +7,7 @@
|
||||
**Web Hosting Control Panel powered by OpenLiteSpeed**
|
||||
Fast • Secure • Scalable — Simplify hosting management with style.
|
||||
|
||||
**Version**: 2.5.5-dev • **Updated**: November 15, 2025
|
||||
**Version**: 2.5.5-dev • **Updated**: January 15, 2026
|
||||
|
||||
[](https://github.com/usmannasir/cyberpanel)
|
||||
[](https://cyberpanel.net/KnowledgeBase/)
|
||||
@@ -70,25 +70,25 @@ Fast • Secure • Scalable — Simplify hosting management with style.
|
||||
|
||||
| OS family | Recommended / Supported |
|
||||
| -------------------------- | ----------------------: |
|
||||
| Ubuntu 24.04, 22.04, 20.04 | ✅ Recommended |
|
||||
| Debian 13, 12, 11 | ✅ Supported |
|
||||
| AlmaLinux 10, 9, 8 | ✅ Supported |
|
||||
| RockyLinux 9, 8 | ✅ Supported |
|
||||
| RHEL 9, 8 | ✅ Supported |
|
||||
| CloudLinux 9, 8 | ✅ Supported |
|
||||
| AlmaLinux 10, 9, 8 | ✅ Recommended |
|
||||
| CentOS 7 | ⚠️ Legacy — EOL |
|
||||
| CloudLinux 9, 8 | ✅ Supported |
|
||||
| Debian 13, 12, 11 | ✅ Supported |
|
||||
| RHEL 9, 8 | ✅ Supported |
|
||||
| RockyLinux 9, 8 | ✅ Supported |
|
||||
| Ubuntu 24.04, 22.04, 20.04 | ✅ Recommended |
|
||||
|
||||
> CyberPanel targets x86\_64 only. Test the unsupported OS in staging first.
|
||||
> **Architectures:** x86_64 (primary), aarch64/ARM64 (supported). AlmaLinux is the recommended RHEL-compatible distribution. Test unsupported OS in staging first.
|
||||
|
||||
---
|
||||
|
||||
## PHP support (short)
|
||||
|
||||
* ✅ **Recommended**: PHP 8.5 (beta), 8.4, 8.3, 8.2, 8.1
|
||||
* ⚠️ **Legacy**: PHP 8.0, PHP 7.4 (security-only)
|
||||
* ❌ **Deprecated**: PHP 7.1, 7.2, 7.3 (no longer installed)
|
||||
* ✅ **Recommended**: PHP 8.5, 8.4
|
||||
* ⚠️ **Security fixes only**: PHP 8.3, 8.2, 8.1
|
||||
* ❌ **EOL / Deprecated**: PHP 8.0, 7.4, 7.1, 7.2, 7.3 (no longer supported)
|
||||
|
||||
Third-party repositories (Remi, Ondrej) may provide older or niche versions; verify compatibility before use.
|
||||
Third-party repositories may provide older or niche versions; verify compatibility before use. RHEL/Alma/Rocky: [Remi RPM](https://rpms.remirepo.net/). Ubuntu/Debian: [Ondrej PPA](https://launchpad.net/~ondrej/+archive/ubuntu/php). See [php.net/supported-versions](https://www.php.net/supported-versions.php).
|
||||
|
||||
---
|
||||
|
||||
@@ -112,6 +112,34 @@ sh <(curl -s https://raw.githubusercontent.com/usmannasir/cyberpanel/stable/preU
|
||||
|
||||
---
|
||||
|
||||
## Upgrade to v2.5.5-dev (non-interactive)
|
||||
|
||||
Upgrade to v2.5.5-dev without branch or MariaDB prompts.
|
||||
|
||||
**MariaDB version options:** `10.11`, `11.8` (LTS default), `12.1` (latest). Use `--mariadb` for 10.11, or `--mariadb-version X` to choose explicitly. If you want to **default to 11.8** and skip the prompt, use `--mariadb-version 11.8`.
|
||||
|
||||
```bash
|
||||
# Upgrade to v2.5.5-dev without prompts (script will prompt for MariaDB unless you pass a flag)
|
||||
sh <(curl -s https://raw.githubusercontent.com/usmannasir/cyberpanel/v2.5.5-dev/preUpgrade.sh || wget -O - https://raw.githubusercontent.com/usmannasir/cyberpanel/v2.5.5-dev/preUpgrade.sh) -b v2.5.5-dev
|
||||
|
||||
# Default to MariaDB 11.8 (LTS) — recommended, non-interactive
|
||||
sh <(curl -s https://raw.githubusercontent.com/usmannasir/cyberpanel/v2.5.5-dev/preUpgrade.sh || wget -O - https://raw.githubusercontent.com/usmannasir/cyberpanel/v2.5.5-dev/preUpgrade.sh) -b v2.5.5-dev --mariadb-version 11.8
|
||||
|
||||
# MariaDB 10.11 (non-interactive)
|
||||
sh <(curl -s https://raw.githubusercontent.com/usmannasir/cyberpanel/v2.5.5-dev/preUpgrade.sh || wget -O - https://raw.githubusercontent.com/usmannasir/cyberpanel/v2.5.5-dev/preUpgrade.sh) -b v2.5.5-dev --mariadb
|
||||
|
||||
# MariaDB 12.1 (latest)
|
||||
sh <(curl -s https://raw.githubusercontent.com/usmannasir/cyberpanel/v2.5.5-dev/preUpgrade.sh || wget -O - https://raw.githubusercontent.com/usmannasir/cyberpanel/v2.5.5-dev/preUpgrade.sh) -b v2.5.5-dev --mariadb-version 12.1
|
||||
```
|
||||
|
||||
**Full non-interactive (v2.5.5-dev + MariaDB 11.8):**
|
||||
|
||||
```bash
|
||||
sh <(curl -s https://raw.githubusercontent.com/usmannasir/cyberpanel/v2.5.5-dev/preUpgrade.sh || wget -O - https://raw.githubusercontent.com/usmannasir/cyberpanel/v2.5.5-dev/preUpgrade.sh) -b v2.5.5-dev --mariadb-version 11.8
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting (common)
|
||||
|
||||
**Command not found** — install curl/wget/git/python3
|
||||
@@ -144,10 +172,27 @@ journalctl -u lscpd -f
|
||||
|
||||
## Recent fixes
|
||||
|
||||
* **02.02.2026** — Plugin updates: premiumPlugin & paypalPremiumPlugin unified verification (Plugin Grants, activation key, Patreon, PayPal, AES-256-CBC encryption). Installed Plugins UI: bulk activate/deactivate, freshness badges, removed Patreon messaging from front.
|
||||
* **15.11.2025** — Hardened MySQL password rotation: `mysqlUtilities.changePassword` now auto-resolves the backing MySQL account (user + host) even when `DBUsers` metadata is missing, preventing the historical `[mysqlUtilities.changePassword] can only concatenate str (not "int")` error. Regression tests live under `Test/mysqlUtilities/`, and you should restart `lscpd` after deploying the patch so the helper reloads.
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### OLS Feature Test Suite
|
||||
|
||||
The OpenLiteSpeed feature test suite (128 tests) validates binary integrity, CyberPanel module, Auto-SSL config, SSL listener auto-mapping, .htaccess processing, ReadApacheConf directives, and more.
|
||||
|
||||
```bash
|
||||
# Run from CyberPanel repo root
|
||||
./tests/ols_test_setup.sh # One-time setup
|
||||
./tests/ols_feature_tests.sh
|
||||
```
|
||||
|
||||
Requires a live CyberPanel + OLS installation.
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
* Official site: [https://cyberpanel.net](https://cyberpanel.net)
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import time
|
||||
|
||||
from .views import VERSION, BUILD
|
||||
|
||||
def version_context(request):
|
||||
@@ -49,4 +52,54 @@ def notification_preferences_context(request):
|
||||
return {
|
||||
'backup_notification_dismissed': False,
|
||||
'ai_scanner_notification_dismissed': False
|
||||
}
|
||||
|
||||
def firewall_static_context(request):
|
||||
"""Expose a cache-busting token for firewall static assets (bumps when firewall.js changes)."""
|
||||
try:
|
||||
from django.conf import settings
|
||||
base = settings.BASE_DIR
|
||||
# Check both app static and repo static so version updates when either is updated
|
||||
paths = [
|
||||
os.path.join(base, 'firewall', 'static', 'firewall', 'firewall.js'),
|
||||
os.path.join(base, 'static', 'firewall', 'firewall.js'),
|
||||
os.path.join(base, 'public', 'static', 'firewall', 'firewall.js'),
|
||||
]
|
||||
version = 0
|
||||
for p in paths:
|
||||
try:
|
||||
version = max(version, int(os.path.getmtime(p)))
|
||||
except (OSError, TypeError):
|
||||
pass
|
||||
if version <= 0:
|
||||
version = int(time.time())
|
||||
except (OSError, AttributeError):
|
||||
version = int(time.time())
|
||||
return {
|
||||
'FIREWALL_STATIC_VERSION': version
|
||||
}
|
||||
|
||||
|
||||
def dns_static_context(request):
|
||||
"""Cache-busting for DNS static assets (bumps when dns.js changes). Avoids stale JS/layout."""
|
||||
try:
|
||||
from django.conf import settings
|
||||
base = settings.BASE_DIR
|
||||
paths = [
|
||||
os.path.join(base, 'dns', 'static', 'dns', 'dns.js'),
|
||||
os.path.join(base, 'static', 'dns', 'dns.js'),
|
||||
os.path.join(base, 'public', 'static', 'dns', 'dns.js'),
|
||||
]
|
||||
version = 0
|
||||
for p in paths:
|
||||
try:
|
||||
version = max(version, int(os.path.getmtime(p)))
|
||||
except (OSError, TypeError):
|
||||
pass
|
||||
if version <= 0:
|
||||
version = int(time.time())
|
||||
except (OSError, AttributeError):
|
||||
version = int(time.time())
|
||||
return {
|
||||
'DNS_STATIC_VERSION': version
|
||||
}
|
||||
589
baseTemplate/static/baseTemplate/assets/mobile-responsive.css
Normal file
589
baseTemplate/static/baseTemplate/assets/mobile-responsive.css
Normal file
@@ -0,0 +1,589 @@
|
||||
/* CyberPanel Mobile Responsive & Readability Fixes */
|
||||
/* This file ensures all pages are mobile-friendly with proper font sizes and readable text */
|
||||
|
||||
/* Base font size and mobile-first approach */
|
||||
html {
|
||||
font-size: 16px; /* Base font size for better readability */
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
color: #2f3640; /* Dark text for better readability on white backgrounds */
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* Ensure all text is readable with proper contrast */
|
||||
* {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Override any light text that might be hard to read */
|
||||
.text-muted, .text-secondary, .text-light {
|
||||
color: #2f3640 !important; /* Dark text for better readability on white backgrounds */
|
||||
}
|
||||
|
||||
/* Fix small font sizes that are hard to read */
|
||||
small, .small, .text-small {
|
||||
font-size: 14px !important; /* Minimum readable size */
|
||||
}
|
||||
|
||||
/* Table improvements for mobile */
|
||||
.table {
|
||||
font-size: 16px !important; /* Larger table text */
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.table th, .table td {
|
||||
padding: 12px 8px !important; /* More padding for touch targets */
|
||||
border: 1px solid #e8e9ff;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
font-size: 14px !important;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.table th {
|
||||
background-color: #f8f9fa;
|
||||
font-weight: 600;
|
||||
color: #2f3640 !important;
|
||||
font-size: 15px !important;
|
||||
}
|
||||
|
||||
/* Button improvements for mobile */
|
||||
.btn {
|
||||
font-size: 16px !important;
|
||||
padding: 12px 20px !important;
|
||||
border-radius: 8px;
|
||||
min-height: 44px; /* Minimum touch target size */
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
font-size: 14px !important;
|
||||
padding: 8px 16px !important;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.btn-xs {
|
||||
font-size: 13px !important;
|
||||
padding: 6px 12px !important;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
/* Form elements */
|
||||
.form-control, input, textarea, select {
|
||||
font-size: 16px !important; /* Prevents zoom on iOS */
|
||||
padding: 12px 16px !important;
|
||||
border: 2px solid #e8e9ff;
|
||||
border-radius: 8px;
|
||||
min-height: 44px;
|
||||
line-height: 1.4;
|
||||
color: #2f3640 !important;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.form-control:focus, input:focus, textarea:focus, select:focus {
|
||||
border-color: #5856d6;
|
||||
box-shadow: 0 0 0 3px rgba(88, 86, 214, 0.1);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Labels and form text */
|
||||
label, .control-label {
|
||||
font-size: 16px !important;
|
||||
font-weight: 600;
|
||||
color: #2f3640 !important;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Headings with proper hierarchy */
|
||||
h1 {
|
||||
font-size: 2.5rem !important; /* 40px */
|
||||
font-weight: 700;
|
||||
color: #1e293b !important;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2rem !important; /* 32px */
|
||||
font-weight: 600;
|
||||
color: #1e293b !important;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 0.875rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.5rem !important; /* 24px */
|
||||
font-weight: 600;
|
||||
color: #2f3640 !important;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.25rem !important; /* 20px */
|
||||
font-weight: 600;
|
||||
color: #2f3640 !important;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.125rem !important; /* 18px */
|
||||
font-weight: 600;
|
||||
color: #2f3640 !important;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem !important; /* 16px */
|
||||
font-weight: 600;
|
||||
color: #2f3640 !important;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Paragraph and body text */
|
||||
p {
|
||||
font-size: 16px !important;
|
||||
line-height: 1.6;
|
||||
color: #2f3640 !important;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Sidebar improvements */
|
||||
#page-sidebar {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
#page-sidebar ul li a {
|
||||
font-size: 16px !important;
|
||||
padding: 12px 20px !important;
|
||||
color: #2f3640 !important;
|
||||
min-height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#page-sidebar ul li a:hover {
|
||||
background-color: #f8f9fa;
|
||||
color: #5856d6 !important;
|
||||
}
|
||||
|
||||
/* Content area improvements */
|
||||
.content-box, .panel, .card {
|
||||
font-size: 16px !important;
|
||||
color: #2f3640 !important;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #e8e9ff;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Modal improvements */
|
||||
.modal-content {
|
||||
font-size: 16px !important;
|
||||
color: #2f3640 !important;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 1.5rem !important;
|
||||
font-weight: 600;
|
||||
color: #1e293b !important;
|
||||
}
|
||||
|
||||
/* Alert and notification improvements */
|
||||
.alert {
|
||||
font-size: 16px !important;
|
||||
padding: 16px 20px !important;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background-color: #f0fdf4;
|
||||
border-color: #bbf7d0;
|
||||
color: #166534 !important;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background-color: #fef2f2;
|
||||
border-color: #fecaca;
|
||||
color: #dc2626 !important;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
background-color: #fffbeb;
|
||||
border-color: #fed7aa;
|
||||
color: #d97706 !important;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background-color: #eff6ff;
|
||||
border-color: #bfdbfe;
|
||||
color: #2563eb !important;
|
||||
}
|
||||
|
||||
/* Navigation improvements */
|
||||
.navbar-nav .nav-link {
|
||||
font-size: 16px !important;
|
||||
padding: 12px 16px !important;
|
||||
color: #2f3640 !important;
|
||||
}
|
||||
|
||||
/* Breadcrumb improvements */
|
||||
.breadcrumb {
|
||||
font-size: 16px !important;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
color: #64748b !important;
|
||||
}
|
||||
|
||||
.breadcrumb-item.active {
|
||||
color: #2f3640 !important;
|
||||
}
|
||||
|
||||
/* Mobile-first responsive breakpoints */
|
||||
@media (max-width: 1200px) {
|
||||
.container, .container-fluid {
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
border: none;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
/* Stack columns on tablets */
|
||||
.col-md-3, .col-md-4, .col-md-6, .col-md-8, .col-md-9 {
|
||||
flex: 0 0 100%;
|
||||
max-width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Adjust sidebar for tablets */
|
||||
#page-sidebar {
|
||||
width: 100%;
|
||||
position: static;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Make tables horizontally scrollable */
|
||||
.table-responsive {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.table {
|
||||
min-width: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
/* Mobile-specific adjustments */
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 14px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.container, .container-fluid {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
/* Stack all columns on mobile */
|
||||
.col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-6, .col-sm-8, .col-sm-9, .col-sm-12,
|
||||
.col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-6, .col-md-8, .col-md-9, .col-md-12 {
|
||||
flex: 0 0 100%;
|
||||
max-width: 100%;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
/* Adjust headings for mobile */
|
||||
h1 {
|
||||
font-size: 2rem !important; /* 32px */
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.75rem !important; /* 28px */
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.5rem !important; /* 24px */
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.25rem !important; /* 20px */
|
||||
}
|
||||
|
||||
/* Button adjustments for mobile */
|
||||
.btn {
|
||||
font-size: 16px !important;
|
||||
padding: 14px 20px !important;
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.btn-group .btn {
|
||||
width: auto;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Form adjustments for mobile */
|
||||
.form-control, input, textarea, select {
|
||||
font-size: 16px !important; /* Prevents zoom on iOS */
|
||||
padding: 14px 16px !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Table adjustments for mobile */
|
||||
.table {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.table th, .table td {
|
||||
padding: 8px 6px !important;
|
||||
font-size: 13px !important;
|
||||
}
|
||||
|
||||
/* Hide less important columns on mobile */
|
||||
.table .d-none-mobile {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Modal adjustments for mobile */
|
||||
.modal-dialog {
|
||||
margin: 10px;
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 20px 15px;
|
||||
}
|
||||
|
||||
/* Content box adjustments */
|
||||
.content-box, .panel, .card {
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
/* Sidebar adjustments for mobile */
|
||||
#page-sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 280px;
|
||||
height: 100vh;
|
||||
z-index: 1000;
|
||||
transition: left 0.3s ease;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 2px 0 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
#page-sidebar.show {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* Main content adjustments when sidebar is open */
|
||||
#main-content {
|
||||
transition: margin-left 0.3s ease;
|
||||
}
|
||||
|
||||
#main-content.sidebar-open {
|
||||
margin-left: 280px;
|
||||
}
|
||||
|
||||
/* Mobile menu toggle */
|
||||
.mobile-menu-toggle {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
z-index: 1001;
|
||||
background-color: #5856d6;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
/* Extra small devices */
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.container, .container-fluid {
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
/* Even smaller buttons and forms for very small screens */
|
||||
.btn {
|
||||
font-size: 14px !important;
|
||||
padding: 12px 16px !important;
|
||||
}
|
||||
|
||||
.form-control, input, textarea, select {
|
||||
font-size: 16px !important; /* Still 16px to prevent zoom */
|
||||
padding: 12px 14px !important;
|
||||
}
|
||||
|
||||
/* Compact table for very small screens */
|
||||
.table th, .table td {
|
||||
padding: 6px 4px !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
/* Hide even more columns on very small screens */
|
||||
.table .d-none-mobile-sm {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Utility classes for mobile */
|
||||
.d-none-mobile {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.d-none-mobile-sm {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.d-none-mobile {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.d-none-mobile-sm {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ensure all text has proper contrast */
|
||||
.text-white {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.text-dark {
|
||||
color: #2f3640 !important;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #2f3640 !important; /* Dark text for better readability */
|
||||
}
|
||||
|
||||
/* Fix any light text on light backgrounds */
|
||||
.bg-light .text-muted,
|
||||
.bg-white .text-muted,
|
||||
.panel .text-muted {
|
||||
color: #2f3640 !important; /* Dark text for better readability */
|
||||
}
|
||||
|
||||
/* Ensure proper spacing for touch targets */
|
||||
a, button, input, select, textarea {
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
}
|
||||
|
||||
/* Additional text readability improvements */
|
||||
/* Fix any green text issues */
|
||||
.ng-binding {
|
||||
color: #2f3640 !important; /* Normal dark text instead of green */
|
||||
}
|
||||
|
||||
/* Ensure all text elements have proper contrast */
|
||||
span, div, p, label, td, th {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Fix specific text color issues */
|
||||
.text-success {
|
||||
color: #059669 !important; /* Darker green for better readability */
|
||||
}
|
||||
|
||||
.text-info {
|
||||
color: #0284c7 !important; /* Darker blue for better readability */
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: #d97706 !important; /* Darker orange for better readability */
|
||||
}
|
||||
|
||||
/* Override Bootstrap's muted text */
|
||||
.text-muted {
|
||||
color: #2f3640 !important; /* Dark text instead of grey */
|
||||
}
|
||||
|
||||
/* Fix any remaining light text on light backgrounds */
|
||||
.bg-white .text-light,
|
||||
.bg-light .text-light,
|
||||
.panel .text-light,
|
||||
.card .text-light {
|
||||
color: #2f3640 !important;
|
||||
}
|
||||
|
||||
/* Fix for small clickable elements */
|
||||
.glyph-icon, .icon {
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Loading and spinner improvements */
|
||||
.spinner, .loading {
|
||||
font-size: 16px !important;
|
||||
color: #5856d6 !important;
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
body {
|
||||
font-size: 12pt;
|
||||
color: #000000 !important;
|
||||
background: #ffffff !important;
|
||||
}
|
||||
|
||||
.table th, .table td {
|
||||
font-size: 10pt !important;
|
||||
color: #000000 !important;
|
||||
}
|
||||
|
||||
.btn, .alert, .modal {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
265
baseTemplate/static/baseTemplate/assets/readability-fixes.css
Normal file
265
baseTemplate/static/baseTemplate/assets/readability-fixes.css
Normal file
@@ -0,0 +1,265 @@
|
||||
/* CyberPanel Readability & Design Fixes */
|
||||
/* This file fixes the core design issues with grey text and color inconsistencies */
|
||||
|
||||
/* Override CSS Variables for Better Text Contrast */
|
||||
:root {
|
||||
/* Ensure all text uses proper dark colors for readability */
|
||||
--text-primary: #2f3640;
|
||||
--text-secondary: #2f3640; /* Changed from grey to dark for better readability */
|
||||
--text-heading: #1e293b;
|
||||
}
|
||||
|
||||
/* Dark theme also uses proper contrast */
|
||||
[data-theme="dark"] {
|
||||
--text-primary: #e4e4e7;
|
||||
--text-secondary: #e4e4e7; /* Changed from grey to light for better readability */
|
||||
--text-heading: #f3f4f6;
|
||||
}
|
||||
|
||||
/* Fix Green Text Issues */
|
||||
/* Override Angular binding colors that might be green */
|
||||
.ng-binding {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
/* Specific fix for uptime display */
|
||||
#sidebar .server-info .info-line span,
|
||||
#sidebar .server-info .info-line .ng-binding,
|
||||
.server-info .ng-binding {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
/* Fix Grey Text on White Background */
|
||||
/* Override all muted and secondary text classes */
|
||||
.text-muted,
|
||||
.text-secondary,
|
||||
.text-light,
|
||||
small,
|
||||
.small,
|
||||
.text-small {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
/* Fix specific Bootstrap classes */
|
||||
.text-muted {
|
||||
color: #2f3640 !important; /* Dark text for better readability */
|
||||
}
|
||||
|
||||
/* Fix text on white/light backgrounds */
|
||||
.bg-white .text-muted,
|
||||
.bg-light .text-muted,
|
||||
.panel .text-muted,
|
||||
.card .text-muted,
|
||||
.content-box .text-muted {
|
||||
color: #2f3640 !important;
|
||||
}
|
||||
|
||||
/* Fix menu items and navigation */
|
||||
#sidebar .menu-item,
|
||||
#sidebar .menu-item span,
|
||||
#sidebar .menu-item i,
|
||||
.sidebar .menu-item,
|
||||
.sidebar .menu-item span,
|
||||
.sidebar .menu-item i {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
#sidebar .menu-item:hover,
|
||||
.sidebar .menu-item:hover {
|
||||
color: var(--accent-color) !important;
|
||||
}
|
||||
|
||||
#sidebar .menu-item.active,
|
||||
.sidebar .menu-item.active {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
/* Fix server info and details */
|
||||
.server-info,
|
||||
.server-info *,
|
||||
.server-details,
|
||||
.server-details *,
|
||||
.info-line,
|
||||
.info-line span,
|
||||
.info-line strong,
|
||||
.tagline,
|
||||
.brand {
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
/* Fix form elements */
|
||||
label,
|
||||
.control-label,
|
||||
.form-label {
|
||||
color: var(--text-primary) !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Fix table text */
|
||||
.table th,
|
||||
.table td {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
.table th {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Fix alert text */
|
||||
.alert {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
color: #059669 !important; /* Darker green for better readability */
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
color: #0284c7 !important; /* Darker blue for better readability */
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
color: #d97706 !important; /* Darker orange for better readability */
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
color: #dc2626 !important; /* Darker red for better readability */
|
||||
}
|
||||
|
||||
/* Fix breadcrumb text */
|
||||
.breadcrumb-item {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
.breadcrumb-item.active {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
/* Fix modal text */
|
||||
.modal-content {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
color: var(--text-heading) !important;
|
||||
}
|
||||
|
||||
/* Fix button text */
|
||||
.btn {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Fix any remaining light text issues */
|
||||
.bg-light .text-light,
|
||||
.bg-white .text-light,
|
||||
.panel .text-light,
|
||||
.card .text-light {
|
||||
color: #2f3640 !important;
|
||||
}
|
||||
|
||||
/* Ensure proper contrast for all text elements */
|
||||
span, div, p, label, td, th, a, li {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Fix specific color classes */
|
||||
.text-success {
|
||||
color: #059669 !important; /* Darker green for better readability */
|
||||
}
|
||||
|
||||
.text-info {
|
||||
color: #0284c7 !important; /* Darker blue for better readability */
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: #d97706 !important; /* Darker orange for better readability */
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #dc2626 !important; /* Darker red for better readability */
|
||||
}
|
||||
|
||||
/* Fix any Angular-specific styling */
|
||||
[ng-controller] {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
[ng-show],
|
||||
[ng-hide],
|
||||
[ng-if] {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Ensure all content areas have proper text color */
|
||||
.content-box,
|
||||
.panel,
|
||||
.card,
|
||||
.main-content,
|
||||
.page-content {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
/* Fix any remaining Bootstrap classes */
|
||||
.text-dark {
|
||||
color: #2f3640 !important;
|
||||
}
|
||||
|
||||
.text-body {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
/* Mobile-specific fixes */
|
||||
@media (max-width: 768px) {
|
||||
/* Ensure mobile text is also readable */
|
||||
body,
|
||||
.container,
|
||||
.container-fluid {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
/* Fix mobile menu text */
|
||||
.mobile-menu .menu-item,
|
||||
.mobile-menu .menu-item span {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
body,
|
||||
.content-box,
|
||||
.panel,
|
||||
.card {
|
||||
color: #000000 !important;
|
||||
background: #ffffff !important;
|
||||
}
|
||||
|
||||
.text-muted,
|
||||
.text-secondary {
|
||||
color: #000000 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* High contrast mode support */
|
||||
@media (prefers-contrast: high) {
|
||||
:root {
|
||||
--text-primary: #000000;
|
||||
--text-secondary: #000000;
|
||||
--text-heading: #000000;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #ffffff;
|
||||
--text-heading: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduced motion support */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ function getCookie(name) {
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
var cookies = document.cookie.split(';');
|
||||
for (var i = 0; i < cookies.length; i++) {
|
||||
var cookie = jQuery.trim(cookies[i]);
|
||||
var cookie = (cookies[i] || '').replace(/^\s+|\s+$/g, '');
|
||||
// Does this cookie string begin with the name we want?
|
||||
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
@@ -39,6 +39,77 @@ function randomPassword(length) {
|
||||
window.app = angular.module('CyberCP', []);
|
||||
var app = window.app; // Local reference for this file
|
||||
|
||||
// MUST be first: register dashboard controller before any other setup (avoids ctrlreg when CDN/Tracking Prevention blocks scripts)
|
||||
app.controller('dashboardStatsController', ['$scope', '$http', '$timeout', function ($scope, $http, $timeout) {
|
||||
$scope.cpuUsage = 0; $scope.ramUsage = 0; $scope.diskUsage = 0; $scope.cpuCores = 0;
|
||||
$scope.ramTotalMB = 0; $scope.diskTotalGB = 0; $scope.diskFreeGB = 0;
|
||||
$scope.totalUsers = 0; $scope.totalSites = 0; $scope.totalWPSites = 0;
|
||||
$scope.totalDBs = 0; $scope.totalEmails = 0; $scope.totalFTPUsers = 0;
|
||||
$scope.topProcesses = []; $scope.sshLogins = []; $scope.sshLogs = [];
|
||||
$scope.loadingTopProcesses = true; $scope.loadingSSHLogins = true; $scope.loadingSSHLogs = true;
|
||||
$scope.blockedIPs = {}; $scope.blockingIP = null; $scope.securityAlerts = [];
|
||||
var opts = { headers: { 'X-CSRFToken': (typeof getCookie === 'function') ? getCookie('csrftoken') : '' } };
|
||||
try {
|
||||
$http.get('/base/getSystemStatus', opts).then(function (r) {
|
||||
if (r && r.data && r.data.status === 1) {
|
||||
$scope.cpuUsage = r.data.cpuUsage || 0; $scope.ramUsage = r.data.ramUsage || 0;
|
||||
$scope.diskUsage = r.data.diskUsage || 0; $scope.cpuCores = r.data.cpuCores || 0;
|
||||
$scope.ramTotalMB = r.data.ramTotalMB || 0; $scope.diskTotalGB = r.data.diskTotalGB || 0;
|
||||
$scope.diskFreeGB = r.data.diskFreeGB || 0;
|
||||
}
|
||||
});
|
||||
$http.get('/base/getDashboardStats', opts).then(function (r) {
|
||||
if (r && r.data && r.data.status === 1) {
|
||||
$scope.totalUsers = r.data.total_users || 0; $scope.totalSites = r.data.total_sites || 0;
|
||||
$scope.totalWPSites = r.data.total_wp_sites || 0; $scope.totalDBs = r.data.total_dbs || 0;
|
||||
$scope.totalEmails = r.data.total_emails || 0; $scope.totalFTPUsers = r.data.total_ftp_users || 0;
|
||||
}
|
||||
});
|
||||
$http.get('/base/getRecentSSHLogins', opts).then(function (r) {
|
||||
$scope.loadingSSHLogins = false;
|
||||
$scope.sshLogins = (r && r.data && r.data.logins) ? r.data.logins : [];
|
||||
}, function () { $scope.loadingSSHLogins = false; $scope.sshLogins = []; });
|
||||
$http.get('/base/getRecentSSHLogs', opts).then(function (r) {
|
||||
$scope.loadingSSHLogs = false;
|
||||
$scope.sshLogs = (r && r.data && r.data.logs) ? r.data.logs : [];
|
||||
}, function () { $scope.loadingSSHLogs = false; $scope.sshLogs = []; });
|
||||
$http.get('/base/getTopProcesses', opts).then(function (r) {
|
||||
$scope.loadingTopProcesses = false;
|
||||
$scope.topProcesses = (r && r.data && r.data.status === 1 && r.data.processes) ? r.data.processes : [];
|
||||
}, function () { $scope.loadingTopProcesses = false; $scope.topProcesses = []; });
|
||||
if (typeof $timeout === 'function') { $timeout(function() { /* refresh */ }, 10000); }
|
||||
} catch (e) { /* ignore */ }
|
||||
}]);
|
||||
|
||||
// Overview CPU/RAM/Disk cards use systemStatusInfo – register early so data loads even if later script fails
|
||||
app.controller('systemStatusInfo', ['$scope', '$http', '$timeout', function ($scope, $http, $timeout) {
|
||||
$scope.uptimeLoaded = false;
|
||||
$scope.uptime = 'Loading...';
|
||||
$scope.cpuUsage = 0; $scope.ramUsage = 0; $scope.diskUsage = 0;
|
||||
$scope.cpuCores = 0; $scope.ramTotalMB = 0; $scope.diskTotalGB = 0; $scope.diskFreeGB = 0;
|
||||
$scope.getSystemStatus = function() { fetchStatus(); };
|
||||
function fetchStatus() {
|
||||
try {
|
||||
var csrf = (typeof getCookie === 'function') ? getCookie('csrftoken') : '';
|
||||
$http.get('/base/getSystemStatus', { headers: { 'X-CSRFToken': csrf } }).then(function (r) {
|
||||
if (r && r.data && r.data.status === 1) {
|
||||
$scope.cpuUsage = r.data.cpuUsage != null ? r.data.cpuUsage : 0;
|
||||
$scope.ramUsage = r.data.ramUsage != null ? r.data.ramUsage : 0;
|
||||
$scope.diskUsage = r.data.diskUsage != null ? r.data.diskUsage : 0;
|
||||
$scope.cpuCores = r.data.cpuCores != null ? r.data.cpuCores : 0;
|
||||
$scope.ramTotalMB = r.data.ramTotalMB != null ? r.data.ramTotalMB : 0;
|
||||
$scope.diskTotalGB = r.data.diskTotalGB != null ? r.data.diskTotalGB : 0;
|
||||
$scope.diskFreeGB = r.data.diskFreeGB != null ? r.data.diskFreeGB : 0;
|
||||
$scope.uptime = r.data.uptime || 'N/A';
|
||||
}
|
||||
$scope.uptimeLoaded = true;
|
||||
}, function() { $scope.uptime = 'Unavailable'; $scope.uptimeLoaded = true; });
|
||||
if (typeof $timeout === 'function') { $timeout(fetchStatus, 60000); }
|
||||
} catch (e) { $scope.uptimeLoaded = true; }
|
||||
}
|
||||
fetchStatus();
|
||||
}]);
|
||||
|
||||
var globalScope;
|
||||
|
||||
function GlobalRespSuccess(response) {
|
||||
@@ -122,9 +193,17 @@ app.controller('systemStatusInfo', function ($scope, $http, $timeout) {
|
||||
|
||||
$scope.uptimeLoaded = false;
|
||||
$scope.uptime = 'Loading...';
|
||||
|
||||
// Defaults so template never shows undefined (avoids raw {$ cpuUsage $} when API is slow or fails)
|
||||
$scope.cpuUsage = 0;
|
||||
$scope.ramUsage = 0;
|
||||
$scope.diskUsage = 0;
|
||||
$scope.cpuCores = 0;
|
||||
$scope.ramTotalMB = 0;
|
||||
$scope.diskTotalGB = 0;
|
||||
$scope.diskFreeGB = 0;
|
||||
|
||||
getStuff();
|
||||
|
||||
|
||||
$scope.getSystemStatus = function() {
|
||||
getStuff();
|
||||
};
|
||||
@@ -138,17 +217,15 @@ app.controller('systemStatusInfo', function ($scope, $http, $timeout) {
|
||||
|
||||
function ListInitialData(response) {
|
||||
|
||||
$scope.cpuUsage = response.data.cpuUsage;
|
||||
$scope.ramUsage = response.data.ramUsage;
|
||||
$scope.diskUsage = response.data.diskUsage;
|
||||
|
||||
// Total system information
|
||||
$scope.cpuCores = response.data.cpuCores;
|
||||
$scope.ramTotalMB = response.data.ramTotalMB;
|
||||
$scope.diskTotalGB = response.data.diskTotalGB;
|
||||
$scope.diskFreeGB = response.data.diskFreeGB;
|
||||
|
||||
// Get uptime if available
|
||||
$scope.cpuUsage = response.data.cpuUsage != null ? response.data.cpuUsage : 0;
|
||||
$scope.ramUsage = response.data.ramUsage != null ? response.data.ramUsage : 0;
|
||||
$scope.diskUsage = response.data.diskUsage != null ? response.data.diskUsage : 0;
|
||||
|
||||
$scope.cpuCores = response.data.cpuCores != null ? response.data.cpuCores : 0;
|
||||
$scope.ramTotalMB = response.data.ramTotalMB != null ? response.data.ramTotalMB : 0;
|
||||
$scope.diskTotalGB = response.data.diskTotalGB != null ? response.data.diskTotalGB : 0;
|
||||
$scope.diskFreeGB = response.data.diskFreeGB != null ? response.data.diskFreeGB : 0;
|
||||
|
||||
if (response.data.uptime) {
|
||||
$scope.uptime = response.data.uptime;
|
||||
$scope.uptimeLoaded = true;
|
||||
@@ -162,6 +239,9 @@ app.controller('systemStatusInfo', function ($scope, $http, $timeout) {
|
||||
function cantLoadInitialData(response) {
|
||||
$scope.uptime = 'Unavailable';
|
||||
$scope.uptimeLoaded = true;
|
||||
$scope.cpuUsage = 0;
|
||||
$scope.ramUsage = 0;
|
||||
$scope.diskUsage = 0;
|
||||
}
|
||||
|
||||
$timeout(getStuff, 60000); // Update every minute
|
||||
@@ -273,11 +353,11 @@ app.controller('adminController', function ($scope, $http, $timeout) {
|
||||
}
|
||||
|
||||
if (!Boolean(response.data.deleteZone)) {
|
||||
$('.addDeleteRecords').hide();
|
||||
$('.deleteZone').hide();
|
||||
}
|
||||
|
||||
if (!Boolean(response.data.addDeleteRecords)) {
|
||||
$('.deleteDatabase').hide();
|
||||
$('.addDeleteRecords').hide();
|
||||
}
|
||||
|
||||
// Email Management
|
||||
@@ -557,15 +637,18 @@ app.controller('homePageStatus', function ($scope, $http, $timeout) {
|
||||
////////////
|
||||
|
||||
function increment() {
|
||||
$('.box').hide();
|
||||
var boxes = document.querySelectorAll ? document.querySelectorAll('.box') : [];
|
||||
for (var i = 0; i < boxes.length; i++) boxes[i].style.display = 'none';
|
||||
setTimeout(function () {
|
||||
$('.box').show();
|
||||
for (var j = 0; j < boxes.length; j++) boxes[j].style.display = '';
|
||||
}, 100);
|
||||
|
||||
|
||||
}
|
||||
|
||||
increment();
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', increment);
|
||||
} else {
|
||||
increment();
|
||||
}
|
||||
|
||||
////////////
|
||||
|
||||
@@ -579,6 +662,7 @@ app.controller('versionManagment', function ($scope, $http, $timeout) {
|
||||
$scope.updateFinish = true;
|
||||
$scope.couldNotConnect = true;
|
||||
|
||||
var upgradeStatusTimer = null;
|
||||
|
||||
$scope.upgrade = function () {
|
||||
|
||||
@@ -660,7 +744,8 @@ app.controller('versionManagment', function ($scope, $http, $timeout) {
|
||||
if (response.data.upgradeStatus === 1) {
|
||||
|
||||
if (response.data.finished === 1) {
|
||||
$timeout.cancel();
|
||||
if (upgradeStatusTimer) $timeout.cancel(upgradeStatusTimer);
|
||||
upgradeStatusTimer = null;
|
||||
$scope.upgradelogBox = false;
|
||||
$scope.upgradeLog = response.data.upgradeLog;
|
||||
$scope.upgradeLoading = true;
|
||||
@@ -672,7 +757,7 @@ app.controller('versionManagment', function ($scope, $http, $timeout) {
|
||||
} else {
|
||||
$scope.upgradelogBox = false;
|
||||
$scope.upgradeLog = response.data.upgradeLog;
|
||||
timeout(getUpgradeStatus, 2000);
|
||||
upgradeStatusTimer = $timeout(getUpgradeStatus, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -900,7 +985,8 @@ app.controller('OnboardingCP', function ($scope, $http, $timeout, $window) {
|
||||
|
||||
});
|
||||
|
||||
app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
||||
// Single implementation registered under both names for compatibility (some templates/caches use newDashboardStat)
|
||||
var dashboardStatsControllerFn = function ($scope, $http, $timeout) {
|
||||
console.log('dashboardStatsController initialized');
|
||||
|
||||
// Card values
|
||||
@@ -920,7 +1006,8 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
||||
$scope.errorTopProcesses = '';
|
||||
$scope.refreshTopProcesses = function() {
|
||||
$scope.loadingTopProcesses = true;
|
||||
$http.get('/base/getTopProcesses').then(function (response) {
|
||||
var h = { headers: { 'X-CSRFToken': (typeof getCookie === 'function') ? getCookie('csrftoken') : '' } };
|
||||
$http.get('/base/getTopProcesses', h).then(function (response) {
|
||||
$scope.loadingTopProcesses = false;
|
||||
if (response.data && response.data.status === 1 && response.data.processes) {
|
||||
$scope.topProcesses = response.data.processes;
|
||||
@@ -939,7 +1026,8 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
||||
$scope.errorSSHLogins = '';
|
||||
$scope.refreshSSHLogins = function() {
|
||||
$scope.loadingSSHLogins = true;
|
||||
$http.get('/base/getRecentSSHLogins').then(function (response) {
|
||||
var h = { headers: { 'X-CSRFToken': (typeof getCookie === 'function') ? getCookie('csrftoken') : '' } };
|
||||
$http.get('/base/getRecentSSHLogins', h).then(function (response) {
|
||||
$scope.loadingSSHLogins = false;
|
||||
if (response.data && response.data.logins) {
|
||||
$scope.sshLogins = response.data.logins;
|
||||
@@ -967,7 +1055,8 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
||||
$scope.loadingSecurityAnalysis = false;
|
||||
$scope.refreshSSHLogs = function() {
|
||||
$scope.loadingSSHLogs = true;
|
||||
$http.get('/base/getRecentSSHLogs').then(function (response) {
|
||||
var h = { headers: { 'X-CSRFToken': (typeof getCookie === 'function') ? getCookie('csrftoken') : '' } };
|
||||
$http.get('/base/getRecentSSHLogs', h).then(function (response) {
|
||||
$scope.loadingSSHLogs = false;
|
||||
if (response.data && response.data.logs) {
|
||||
$scope.sshLogs = response.data.logs;
|
||||
@@ -1073,19 +1162,8 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
||||
return; // Already processing this IP
|
||||
}
|
||||
|
||||
// Check if already blocked
|
||||
if ($scope.blockedIPs && $scope.blockedIPs[ipAddress]) {
|
||||
console.log('IP already blocked:', ipAddress);
|
||||
if (typeof PNotify !== 'undefined') {
|
||||
new PNotify({
|
||||
title: 'Info',
|
||||
text: `IP address ${ipAddress} is already banned`,
|
||||
type: 'info',
|
||||
delay: 3000
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Do not early-return when IP is already in blockedIPs: still call the API so the
|
||||
// backend can close any active connections from this IP (already-banned path).
|
||||
|
||||
// Set blocking flag to prevent duplicate requests
|
||||
$scope.blockingIP = ipAddress;
|
||||
@@ -1130,19 +1208,12 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
||||
console.log('Parsed responseData from string:', responseData);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse response as JSON:', e);
|
||||
console.error('Raw response string:', responseData);
|
||||
// Try to extract error from string
|
||||
if (responseData.includes('error')) {
|
||||
if (typeof PNotify !== 'undefined') {
|
||||
new PNotify({
|
||||
title: 'Error',
|
||||
text: 'Failed to block IP address: ' + responseData,
|
||||
type: 'error',
|
||||
delay: 5000
|
||||
});
|
||||
}
|
||||
return;
|
||||
var errorMsg = responseData && responseData.length ? responseData : 'Failed to block IP address';
|
||||
if (typeof PNotify !== 'undefined') {
|
||||
new PNotify({ title: 'Error', text: errorMsg, type: 'error', delay: 5000 });
|
||||
}
|
||||
$scope.blockingIP = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1159,11 +1230,12 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
||||
}
|
||||
$scope.blockedIPs[ipAddress] = true;
|
||||
|
||||
// Show success notification
|
||||
// Show success notification (use server message when present, e.g. already-banned + connections closed)
|
||||
if (typeof PNotify !== 'undefined') {
|
||||
var successText = (responseData.message && responseData.message.length) ? responseData.message : `IP address ${ipAddress} has been permanently banned and added to the firewall. You can manage it in the Firewall > Banned IPs section.`;
|
||||
new PNotify({
|
||||
title: 'IP Address Banned',
|
||||
text: `IP address ${ipAddress} has been permanently banned and added to the firewall. You can manage it in the Firewall > Banned IPs section.`,
|
||||
text: successText,
|
||||
type: 'success',
|
||||
delay: 5000
|
||||
});
|
||||
@@ -1217,28 +1289,20 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
||||
$scope.lastErrorTime = Date.now();
|
||||
|
||||
var errorMessage = 'Failed to block IP address';
|
||||
if (err.data) {
|
||||
var errData = err.data;
|
||||
if (typeof errData === 'string') {
|
||||
try {
|
||||
errData = JSON.parse(errData);
|
||||
} catch (e) {
|
||||
errorMessage = errData || errorMessage;
|
||||
var errData = err.data;
|
||||
if (typeof errData === 'string') {
|
||||
try {
|
||||
errData = JSON.parse(errData);
|
||||
} catch (e) {
|
||||
if (errData && errData.length) {
|
||||
errorMessage = errData.length > 200 ? errData.substring(0, 200) + '...' : errData;
|
||||
}
|
||||
}
|
||||
if (errData && typeof errData === 'object') {
|
||||
if (errData.error_message) {
|
||||
errorMessage = errData.error_message;
|
||||
} else if (errData.error) {
|
||||
errorMessage = errData.error;
|
||||
} else if (errData.message) {
|
||||
errorMessage = errData.message;
|
||||
}
|
||||
}
|
||||
} else if (err.statusText) {
|
||||
errorMessage = err.statusText;
|
||||
}
|
||||
if (errData && typeof errData === 'object') {
|
||||
errorMessage = errData.error_message || errData.error || errData.message || errorMessage;
|
||||
} else if (err.status) {
|
||||
errorMessage = `HTTP ${err.status}: ${err.statusText || 'Unknown error'}`;
|
||||
errorMessage = 'HTTP ' + err.status + ': ' + (errorMessage);
|
||||
}
|
||||
|
||||
console.error('Final error message:', errorMessage);
|
||||
@@ -1287,14 +1351,9 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
||||
return; // Already processing
|
||||
}
|
||||
|
||||
if ($scope.blockedIPs[ipAddress]) {
|
||||
new PNotify({
|
||||
title: 'Info',
|
||||
text: `IP address ${ipAddress} is already banned`,
|
||||
type: 'info',
|
||||
delay: 3000
|
||||
});
|
||||
return;
|
||||
// Still call API when already in blockedIPs so backend can close active connections
|
||||
if (!$scope.blockedIPs) {
|
||||
$scope.blockedIPs = {};
|
||||
}
|
||||
|
||||
$scope.blockingIP = ipAddress;
|
||||
@@ -1380,14 +1439,9 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
||||
return; // Already processing
|
||||
}
|
||||
|
||||
if ($scope.blockedIPs[ipAddress]) {
|
||||
new PNotify({
|
||||
title: 'Info',
|
||||
text: `IP address ${ipAddress} is already banned`,
|
||||
type: 'info',
|
||||
delay: 3000
|
||||
});
|
||||
return;
|
||||
// Still call API when already in blockedIPs so backend can close active connections
|
||||
if (!$scope.blockedIPs) {
|
||||
$scope.blockedIPs = {};
|
||||
}
|
||||
|
||||
$scope.blockingIP = ipAddress;
|
||||
@@ -1474,6 +1528,12 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
||||
var pollInterval = 2000; // ms
|
||||
var maxPoints = 30;
|
||||
|
||||
// Expose so switchTab can create charts on first tab click if they weren't created at load
|
||||
window.cyberPanelSetupChartsIfNeeded = function() {
|
||||
if (window.trafficChart && window.diskIOChart && window.cpuChart) return;
|
||||
try { setupCharts(); } catch (e) { console.error('cyberPanelSetupChartsIfNeeded:', e); }
|
||||
};
|
||||
|
||||
function pollDashboardStats() {
|
||||
console.log('[dashboardStatsController] pollDashboardStats() called');
|
||||
console.log('[dashboardStatsController] Fetching dashboard stats from /base/getDashboardStats');
|
||||
@@ -1532,8 +1592,8 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
||||
}
|
||||
|
||||
function pollTraffic() {
|
||||
console.log('pollTraffic called');
|
||||
$http.get('/base/getTrafficStats').then(function(response) {
|
||||
if (!response || !response.data) return;
|
||||
if (response.data.admin_only) {
|
||||
// Hide chart for non-admin users
|
||||
$scope.hideSystemCharts = true;
|
||||
@@ -1581,13 +1641,16 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
||||
}
|
||||
lastRx = rx; lastTx = tx;
|
||||
} else {
|
||||
console.log('pollTraffic error or no data:', response);
|
||||
console.warn('pollTraffic: no data or status', response.data);
|
||||
}
|
||||
}).catch(function(err) {
|
||||
console.warn('pollTraffic failed:', err);
|
||||
});
|
||||
}
|
||||
|
||||
function pollDiskIO() {
|
||||
$http.get('/base/getDiskIOStats').then(function(response) {
|
||||
if (!response || !response.data) return;
|
||||
if (response.data.admin_only) {
|
||||
// Hide chart for non-admin users
|
||||
$scope.hideSystemCharts = true;
|
||||
@@ -1626,11 +1689,14 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
||||
}
|
||||
lastDiskRead = read; lastDiskWrite = write;
|
||||
}
|
||||
}).catch(function(err) {
|
||||
console.warn('pollDiskIO failed:', err);
|
||||
});
|
||||
}
|
||||
|
||||
function pollCPU() {
|
||||
$http.get('/base/getCPULoadGraph').then(function(response) {
|
||||
if (!response || !response.data) return;
|
||||
if (response.data.admin_only) {
|
||||
// Hide chart for non-admin users
|
||||
$scope.hideSystemCharts = true;
|
||||
@@ -1669,13 +1735,34 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
||||
}
|
||||
lastCPUTimes = cpuTimes;
|
||||
}
|
||||
}).catch(function(err) {
|
||||
console.warn('pollCPU failed:', err);
|
||||
});
|
||||
}
|
||||
|
||||
function setupCharts() {
|
||||
console.log('setupCharts called, initializing charts...');
|
||||
var trafficCtx = document.getElementById('trafficChart').getContext('2d');
|
||||
trafficChart = new Chart(trafficCtx, {
|
||||
function setupCharts(retryCount) {
|
||||
retryCount = retryCount || 0;
|
||||
if (typeof Chart === 'undefined') {
|
||||
if (retryCount < 3) {
|
||||
$timeout(function() { setupCharts(retryCount + 1); }, 400);
|
||||
}
|
||||
return;
|
||||
}
|
||||
var trafficEl = document.getElementById('trafficChart');
|
||||
if (!trafficEl) {
|
||||
if (retryCount < 5) {
|
||||
$timeout(function() { setupCharts(retryCount + 1); }, 300);
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var trafficCtx = trafficEl.getContext('2d');
|
||||
} catch (e) {
|
||||
console.error('trafficChart getContext failed:', e);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
trafficChart = new Chart(trafficCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
@@ -1767,7 +1854,9 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
||||
console.log('trafficChart resized and updated after setup.');
|
||||
}
|
||||
}, 500);
|
||||
var diskCtx = document.getElementById('diskIOChart').getContext('2d');
|
||||
var diskEl = document.getElementById('diskIOChart');
|
||||
if (!diskEl) { console.warn('diskIOChart canvas not found'); return; }
|
||||
var diskCtx = diskEl.getContext('2d');
|
||||
diskIOChart = new Chart(diskCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
@@ -1852,7 +1941,10 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
||||
layout: { padding: { top: 10, bottom: 10, left: 10, right: 10 } }
|
||||
}
|
||||
});
|
||||
var cpuCtx = document.getElementById('cpuChart').getContext('2d');
|
||||
window.diskIOChart = diskIOChart;
|
||||
var cpuEl = document.getElementById('cpuChart');
|
||||
if (!cpuEl) { console.warn('cpuChart canvas not found'); return; }
|
||||
var cpuCtx = cpuEl.getContext('2d');
|
||||
cpuChart = new Chart(cpuCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
@@ -1925,6 +2017,10 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
||||
layout: { padding: { top: 10, bottom: 10, left: 10, right: 10 } }
|
||||
}
|
||||
});
|
||||
window.cpuChart = cpuChart;
|
||||
} catch (e) {
|
||||
console.error('setupCharts error:', e);
|
||||
}
|
||||
|
||||
// Redraw charts on tab shown
|
||||
$("a[data-toggle='tab']").on('shown.bs.tab', function (e) {
|
||||
@@ -1957,19 +2053,20 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
||||
$scope.refreshSSHLogs();
|
||||
|
||||
$timeout(function() {
|
||||
// Check if user is admin before setting up charts
|
||||
// Always create charts so Traffic/Disk IO/CPU tabs have something to show; admin check only affects hideSystemCharts
|
||||
setupCharts();
|
||||
$http.get('/base/getAdminStatus').then(function(response) {
|
||||
if (response.data && response.data.admin === 1) {
|
||||
setupCharts();
|
||||
if (response.data && (response.data.admin === 1 || response.data.admin === true)) {
|
||||
$scope.hideSystemCharts = false;
|
||||
} else {
|
||||
$scope.hideSystemCharts = true;
|
||||
}
|
||||
}).catch(function() {
|
||||
// If error, assume non-admin and hide charts
|
||||
}).catch(function(err) {
|
||||
console.warn('getAdminStatus failed:', err);
|
||||
$scope.hideSystemCharts = true;
|
||||
});
|
||||
|
||||
// Start polling for all stats
|
||||
// Start polling for all stats (data feeds charts)
|
||||
function pollAll() {
|
||||
pollDashboardStats();
|
||||
pollTraffic();
|
||||
@@ -1979,7 +2076,7 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
||||
$timeout(pollAll, pollInterval);
|
||||
}
|
||||
pollAll();
|
||||
}, 500);
|
||||
}, 800);
|
||||
|
||||
// SSH User Activity Modal
|
||||
$scope.showSSHActivityModal = false;
|
||||
@@ -2425,4 +2522,6 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
||||
$scope.closeSSHActivityModal();
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
app.controller('dashboardStatsController', dashboardStatsControllerFn);
|
||||
app.controller('newDashboardStat', dashboardStatsControllerFn);
|
||||
2
baseTemplate/static/baseTemplate/vendor/select2/select2.full.min.js
vendored
Normal file
2
baseTemplate/static/baseTemplate/vendor/select2/select2.full.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
baseTemplate/static/baseTemplate/vendor/select2/select2.min.css
vendored
Normal file
1
baseTemplate/static/baseTemplate/vendor/select2/select2.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -11,7 +11,7 @@
|
||||
<link rel="icon" type="image/png" href="{% static 'baseTemplate/assets/finalBase/favicon.png' %}">
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-beta.2/css/bootstrap.min.css" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="{% static 'filemanager/images/fonts/css/font-awesome.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'filemanager/css/fileManager.css' %}">
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
</div-->
|
||||
<ul class="nav mr-10">
|
||||
<li class="nav-item">
|
||||
<a onclick="return false;" ng-click="showUploadBox()" class="nav-link point-events" href="#"><i class="fa fa-upload" aria-hidden="true"></i> {% trans "Upload" %}</a>
|
||||
<a id="uploadTriggerBtn" onclick="return false;" ng-click="showUploadBox()" class="nav-link point-events" href="#"><i class="fa fa-upload" aria-hidden="true"></i> {% trans "Upload" %}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a onclick="return false;" ng-click="showCreateFileModal()" class="nav-link point-events" href="#"><i class="fa fa-plus-square" aria-hidden="true"></i> {% trans "New File" %}</a>
|
||||
@@ -608,7 +608,7 @@
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js" integrity="sha384-vFJXuSJphROIrBnz7yo7oB41mKfc8JzQZiCq4NCceLEaO4IHwicKwpJf9c9IpFgh" crossorigin="anonymous"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js" integrity="sha384-alpBpkh1PFOepccYVYDB4do5UnbKysX5WZXm3XxPqe5iKTfUKjNkCk9SaVuEZflJ" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-beta.2/js/bootstrap.min.js" integrity="sha384-alpBpkh1PFOepccYVYDB4do5UnbKysX5WZXm3XxPqe5iKTfUKjNkCk9SaVuEZflJ" crossorigin="anonymous"></script>
|
||||
|
||||
|
||||
<!-- HTML Editor Include -->
|
||||
|
||||
@@ -876,6 +876,29 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- SSH Logins Pagination -->
|
||||
<div ng-if="!loadingSSHLogins && sshLogins.length > 0" class="pagination-controls" style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; margin-top: 16px; padding: 12px 0; border-top: 1px solid #e8e9ff;">
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<span style="color: #64748b; font-size: 13px;">Show</span>
|
||||
<select ng-model="sshLoginsPerPage" ng-change="sshLoginsChangePerPage()" style="padding: 6px 10px; border: 1px solid #e2e8f0; border-radius: 6px; font-size: 13px; background: white;">
|
||||
<option value="10">10</option>
|
||||
<option value="20">20</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
<span style="color: #64748b; font-size: 13px;">per page</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<span style="color: #64748b; font-size: 13px;">{$ (sshLoginsPage - 1) * sshLoginsPerPage + 1 $}-{$ (sshLoginsPage * sshLoginsPerPage > sshLoginsTotal ? sshLoginsTotal : sshLoginsPage * sshLoginsPerPage) $} of {$ sshLoginsTotal $}</span>
|
||||
<button ng-click="sshLoginsGoToPage(sshLoginsPage - 1)" ng-disabled="sshLoginsPage <= 1" style="padding: 6px 12px; border: 1px solid #e2e8f0; border-radius: 6px; background: white; cursor: pointer; font-size: 13px;" title="Previous">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</button>
|
||||
<button ng-click="sshLoginsGoToPage(sshLoginsPage + 1)" ng-disabled="sshLoginsPage >= sshLoginsTotalPages" style="padding: 6px 12px; border: 1px solid #e2e8f0; border-radius: 6px; background: white; cursor: pointer; font-size: 13px;" title="Next">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dummy data for demonstration -->
|
||||
<table class="activity-table records-table" ng-if="loadingSSHLogins">
|
||||
<thead>
|
||||
@@ -1065,6 +1088,29 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- SSH Logs Pagination -->
|
||||
<div ng-if="!loadingSSHLogs && sshLogs.length > 0" class="pagination-controls" style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; margin-top: 16px; padding: 12px 0; border-top: 1px solid #e8e9ff;">
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<span style="color: #64748b; font-size: 13px;">Show</span>
|
||||
<select ng-model="sshLogsPerPage" ng-change="sshLogsChangePerPage()" style="padding: 6px 10px; border: 1px solid #e2e8f0; border-radius: 6px; font-size: 13px; background: white;">
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
<span style="color: #64748b; font-size: 13px;">per page</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<span style="color: #64748b; font-size: 13px;">{$ (sshLogsPage - 1) * sshLogsPerPage + 1 $}-{$ (sshLogsPage * sshLogsPerPage > sshLogsTotal ? sshLogsTotal : sshLogsPage * sshLogsPerPage) $} of {$ sshLogsTotal $}</span>
|
||||
<button ng-click="sshLogsGoToPage(sshLogsPage - 1)" ng-disabled="sshLogsPage <= 1" style="padding: 6px 12px; border: 1px solid #e2e8f0; border-radius: 6px; background: white; cursor: pointer; font-size: 13px;" title="Previous">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</button>
|
||||
<button ng-click="sshLogsGoToPage(sshLogsPage + 1)" ng-disabled="sshLogsPage >= sshLogsTotalPages" style="padding: 6px 12px; border: 1px solid #e2e8f0; border-radius: 6px; background: white; cursor: pointer; font-size: 13px;" title="Next">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Process Tab -->
|
||||
@@ -1311,11 +1357,25 @@
|
||||
// Add active class to clicked tab
|
||||
tabButton.classList.add('active');
|
||||
|
||||
// Trigger chart resize if switching to chart tabs
|
||||
// Chart tabs: ensure charts exist (lazy init on first click), then resize/update
|
||||
if (tabId === 'traffic' || tabId === 'diskio' || tabId === 'cpu-usage') {
|
||||
if (typeof window.cyberPanelSetupChartsIfNeeded === 'function') {
|
||||
window.cyberPanelSetupChartsIfNeeded();
|
||||
}
|
||||
setTimeout(() => {
|
||||
var ch;
|
||||
if (tabId === 'traffic' && (ch = window.trafficChart) && typeof ch.resize === 'function') {
|
||||
ch.resize();
|
||||
if (typeof ch.update === 'function') ch.update();
|
||||
} else if (tabId === 'diskio' && (ch = window.diskIOChart) && typeof ch.resize === 'function') {
|
||||
ch.resize();
|
||||
if (typeof ch.update === 'function') ch.update();
|
||||
} else if (tabId === 'cpu-usage' && (ch = window.cpuChart) && typeof ch.resize === 'function') {
|
||||
ch.resize();
|
||||
if (typeof ch.update === 'function') ch.update();
|
||||
}
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
}, 100);
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1334,56 +1394,38 @@
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '/base/blockIPAddress',
|
||||
url: '/firewall/addBannedIP',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
headers: {
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
},
|
||||
data: JSON.stringify({
|
||||
'ip_address': ipAddress,
|
||||
'reason': 'Security alert detected from dashboard'
|
||||
'ip': ipAddress,
|
||||
'reason': 'Brute force attack detected from SSH Security Analysis',
|
||||
'duration': 'permanent'
|
||||
}),
|
||||
success: function(data) {
|
||||
// Handle both success and error responses
|
||||
if (data.status === 1) {
|
||||
showNotification('success', data.message || 'IP address blocked successfully');
|
||||
if (data && data.status === 1) {
|
||||
showNotification('success', data.message || 'IP address blocked successfully. Manage in Firewall > Banned IPs.');
|
||||
// Refresh the page to update the blocked IPs list
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 1000);
|
||||
setTimeout(function() { location.reload(); }, 1000);
|
||||
} else {
|
||||
// Handle error response - check for both 'error' and 'error_message' fields
|
||||
var errorMsg = data.error || data.error_message || data.message || 'Failed to block IP address';
|
||||
var errorMsg = (data && (data.error_message || data.error || data.message)) || 'Failed to block IP address';
|
||||
showNotification('error', errorMsg);
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
// Handle network errors and parse JSON errors
|
||||
console.error('Ban IP error:', xhr, status, error);
|
||||
console.error('Ban IP error:', xhr.status, xhr.responseText);
|
||||
var errorMsg = 'Failed to block IP address. Please try again.';
|
||||
|
||||
// Log full response for debugging
|
||||
console.log('Response status:', xhr.status);
|
||||
console.log('Response text:', xhr.responseText);
|
||||
|
||||
if (xhr.responseJSON) {
|
||||
errorMsg = xhr.responseJSON.error || xhr.responseJSON.error_message || xhr.responseJSON.message || errorMsg;
|
||||
console.log('Parsed error from JSON:', errorMsg);
|
||||
} else if (xhr.responseText) {
|
||||
try {
|
||||
var errorData = JSON.parse(xhr.responseText);
|
||||
errorMsg = errorData.error || errorData.error_message || errorData.message || errorMsg;
|
||||
console.log('Parsed error from text:', errorMsg);
|
||||
} catch(e) {
|
||||
console.error('Failed to parse error response:', e);
|
||||
// If parsing fails, try to extract error from response text
|
||||
if (xhr.responseText.includes('error')) {
|
||||
errorMsg = xhr.responseText.substring(0, 200);
|
||||
}
|
||||
}
|
||||
var data = xhr.responseJSON;
|
||||
if (!data && xhr.responseText) {
|
||||
try { data = JSON.parse(xhr.responseText); } catch(e) {}
|
||||
}
|
||||
if (data && (data.error_message || data.error || data.message)) {
|
||||
errorMsg = data.error_message || data.error || data.message;
|
||||
}
|
||||
|
||||
showNotification('error', errorMsg);
|
||||
},
|
||||
complete: function() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% load i18n %}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
{% with CP_VERSION="2.4.4.1" %}
|
||||
{% with CP_VERSION=CYBERPANEL_FULL_VERSION|default:"2.5.5.dev" %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" ng-app="CyberCP">
|
||||
<head>
|
||||
@@ -26,29 +26,25 @@
|
||||
<!-- Readability Fixes CSS -->
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'baseTemplate/assets/readability-fixes.css' %}?v={{ CP_VERSION }}">
|
||||
|
||||
<!-- Core Scripts (data-cfasync=false prevents Cloudflare Rocket Loader from breaking load order) -->
|
||||
<!-- Core Scripts: Angular + system-status FIRST so dashboard works even when CDN/Tracking Prevention blocks jQuery/Bootstrap -->
|
||||
<script src="{% static 'baseTemplate/angularjs.1.6.5.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'baseTemplate/custom-js/system-status.js' %}?v={{ CP_VERSION }}&dashboard=3" data-cfasync="false"></script>
|
||||
<script src="https://code.jquery.com/jquery-2.2.4.min.js" data-cfasync="false"></script>
|
||||
<!-- Bootstrap JavaScript -->
|
||||
|
||||
<script src="{% static 'baseTemplate/assets/bootstrap/js/bootstrap.min.js' %}?v={{ CP_VERSION }}"></script>
|
||||
<script src="{% static 'baseTemplate/bootstrap-toggle.min.js' %}?v={{ CP_VERSION }}"></script>
|
||||
<script src="{% static 'baseTemplate/custom-js/qrious.min.js' %}?v={{ CP_VERSION }}"></script>
|
||||
<script src="{% static 'baseTemplate/custom-js/system-status.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'baseTemplate/custom-js/chart.umd.min.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
|
||||
<!-- Chart.js -->
|
||||
<script src="{% static 'baseTemplate/custom-js/chart.umd.min.js' %}?v={{ CP_VERSION }}"></script>
|
||||
|
||||
<!-- PNotify (data-cfasync=false ensures it loads before controllers that use it) -->
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'baseTemplate/custom-js/pnotify.custom.min.css' %}?v={{ CP_VERSION }}">
|
||||
<script src="{% static 'baseTemplate/custom-js/pnotify.custom.min.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
|
||||
<!-- Select2 (required by FTP create account; load after jQuery; exclude from Rocket Loader to preserve order) -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/select2@4.0.13/dist/css/select2.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.0.13/dist/js/select2.full.min.js" data-cfasync="false"></script>
|
||||
<!-- Select2 (required by FTP create account; local copy avoids CDN Tracking Prevention blocking) -->
|
||||
<link rel="stylesheet" href="{% static 'baseTemplate/vendor/select2/select2.min.css' %}?v={{ CP_VERSION }}">
|
||||
<script src="{% static 'baseTemplate/vendor/select2/select2.full.min.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
|
||||
<!-- Modern Design System -->
|
||||
<style>
|
||||
@@ -297,38 +293,51 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
/* Notification dropdown: always light panel with dark text so readable in both light and dark mode */
|
||||
.notification-center-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 10px);
|
||||
right: 0;
|
||||
background: white;
|
||||
border: 1px solid var(--border-color);
|
||||
background: #ffffff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
|
||||
width: 520px;
|
||||
max-width: calc(100vw - 40px);
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
z-index: 10000;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
z-index: 10000;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
.notification-center-dropdown.show { display: block; }
|
||||
.notification-center-header {
|
||||
.notification-center-dropdown.show { display: flex; }
|
||||
.notification-center-dropdown .notification-center-header {
|
||||
padding: 1.25rem 1.5rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.notification-center-header h3 { margin: 0; font-size: 1.25rem; font-weight: 700; }
|
||||
.notification-center-list { padding: 1rem; }
|
||||
.notification-center-empty { color: var(--text-secondary); padding: 1rem; }
|
||||
.notification-center-dropdown .notification-center-header h3 { margin: 0; font-size: 1.25rem; font-weight: 700; color: #111827; }
|
||||
.notification-center-list {
|
||||
padding: 1rem;
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
max-height: 480px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
#notification-center-dropdown .notification-center-empty { color: #4b5563; padding: 1rem; }
|
||||
.notification-center-item {
|
||||
padding: 1.5rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 1rem;
|
||||
background: white;
|
||||
background: #ffffff;
|
||||
overflow: visible;
|
||||
}
|
||||
.notification-center-item.dismissed { opacity: 0.7; background: #f9fafb; }
|
||||
.notification-center-item-title {
|
||||
@@ -337,36 +346,69 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
color: #111827;
|
||||
}
|
||||
.notification-center-item-title .dismissed-badge {
|
||||
font-size: 0.75rem;
|
||||
color: #6b7280;
|
||||
color: #374151;
|
||||
margin-left: auto;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: #f3f4f6;
|
||||
background: #e5e7eb;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.notification-center-item-text { color: var(--text-secondary); font-size: 0.95rem; margin-bottom: 1rem; line-height: 1.6; }
|
||||
.notification-center-item-actions { display: flex; gap: 0.75rem; flex-wrap: wrap; }
|
||||
.notification-center-item-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: white;
|
||||
background: linear-gradient(135deg, var(--accent-color) 0%, #6d6bd4 100%);
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 10px;
|
||||
#notification-center-dropdown .notification-center-item-text { color: #4b5563; font-size: 0.95rem; margin-bottom: 1rem; line-height: 1.6; }
|
||||
#notification-center-dropdown .notification-center-item-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
align-items: flex-start;
|
||||
margin-top: 0.75rem;
|
||||
width: 100%;
|
||||
}
|
||||
.notification-center-item-link-secondary {
|
||||
#notification-center-dropdown .notification-center-item-actions .notification-center-item-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--accent-color);
|
||||
justify-content: center;
|
||||
gap: 0.4rem;
|
||||
color: #ffffff !important;
|
||||
background: linear-gradient(135deg, #4f46e5 0%, #4338ca 100%) !important;
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
padding: 0.5rem 1.25rem;
|
||||
border-radius: 8px;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
min-width: 160px;
|
||||
width: auto;
|
||||
min-height: 2.25rem;
|
||||
line-height: 1.3;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
#notification-center-dropdown .notification-center-item-actions .notification-center-item-link:hover {
|
||||
filter: brightness(1.1);
|
||||
color: #ffffff !important;
|
||||
}
|
||||
#notification-center-dropdown .notification-center-item-actions .notification-center-item-link-secondary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
color: #4338ca !important;
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
padding: 0.4rem 0.75rem;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
min-width: 120px;
|
||||
width: auto;
|
||||
min-height: 1.75rem;
|
||||
line-height: 1.3;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
#notification-center-dropdown .notification-center-item-actions .notification-center-item-link-secondary:hover {
|
||||
text-decoration: underline;
|
||||
color: #3730a3 !important;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
@@ -1547,9 +1589,6 @@
|
||||
<a href="{% url 'listChildDomains' %}" class="menu-item">
|
||||
<span>List Sub/Addon Domains</span>
|
||||
</a>
|
||||
<a href="{% url 'fixSubdomainLogs' %}" class="menu-item">
|
||||
<span>Fix Subdomain Logs</span>
|
||||
</a>
|
||||
{% if admin or modifyWebsite %}
|
||||
<a href="{% url 'modifyWebsite' %}" class="menu-item">
|
||||
<span>Modify Website</span>
|
||||
@@ -1651,22 +1690,22 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if admin or deleteZone %}
|
||||
<a href="{% url 'deleteDNSZone' %}" class="menu-item">
|
||||
<a href="{% url 'deleteDNSZone' %}" class="menu-item deleteZone">
|
||||
<span>Delete Zone</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if admin or addDeleteRecords %}
|
||||
<a href="{% url 'addDeleteDNSRecords' %}" class="menu-item">
|
||||
<a href="{% url 'addDeleteDNSRecords' %}" class="menu-item addDeleteRecords">
|
||||
<span>Add/Delete Records</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if admin or addDeleteRecords %}
|
||||
<a href="{% url 'addDeleteDNSRecordsCloudFlare' %}" class="menu-item">
|
||||
<a href="{% url 'addDeleteDNSRecordsCloudFlare' %}" class="menu-item addDeleteRecords">
|
||||
<span>CloudFlare</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if admin or addDeleteRecords %}
|
||||
<a href="{% url 'ResetDNSConfigurations' %}" class="menu-item">
|
||||
<a href="{% url 'ResetDNSConfigurations' %}" class="menu-item addDeleteRecords">
|
||||
<span>Reset DNS Configurations</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
@@ -1835,9 +1874,7 @@
|
||||
<a href="{% url 'manageSSL' %}" class="menu-item">
|
||||
<span>Manage SSL</span>
|
||||
</a>
|
||||
<a href="{% url 'sslReconcile' %}" class="menu-item">
|
||||
<span>SSL Reconciliation</span>
|
||||
</a>
|
||||
{% comment %}SSL Reconciliation - hidden; URL /manageSSL/sslReconcile still works if needed{% endcomment %}
|
||||
{% endif %}
|
||||
{% if admin or hostnameSSL %}
|
||||
<a href="{% url 'sslForHostName' %}" class="menu-item">
|
||||
@@ -2186,14 +2223,14 @@
|
||||
<script src="{% static 'websiteFunctions/websiteFunctions.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'userManagment/userManagment.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'databases/databases.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'dns/dns.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'dns/dns.js' %}?v={{ CP_VERSION }}&dns={{ DNS_STATIC_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'mailServer/mailServer.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'ftp/ftp.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'backup/backup.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'managePHP/managePHP.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'serverLogs/serverLogs.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'serverStatus/serverStatus.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'firewall/firewall.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'firewall/firewall.js' %}?v={{ CP_VERSION }}&fw={{ FIREWALL_STATIC_VERSION|default:CP_VERSION }}&cb=4" data-cfasync="false"></script>
|
||||
<script src="{% static 'emailPremium/emailPremium.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'manageServices/manageServices.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'CLManager/CLManager.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
@@ -2305,10 +2342,7 @@
|
||||
|
||||
// Check if user has backup configured (you'll need to implement this API)
|
||||
// For now, we'll show it by default unless they have a backup plan
|
||||
// This should be replaced with an actual check
|
||||
{% if not request.session.has_backup_configured %}
|
||||
showBackupNotification();
|
||||
{% endif %}
|
||||
// Session check omitted - has_backup_configured may be absent in some views
|
||||
|
||||
// For demonstration, let's show it if URL doesn't contain 'OneClickBackups'
|
||||
if (!window.location.href.includes('OneClickBackups')) {
|
||||
@@ -2400,12 +2434,13 @@
|
||||
// .htaccess Feature Notification Functions
|
||||
function checkHtaccessStatus() {
|
||||
// Check if user has dismissed the notification permanently (localStorage for longer persistence)
|
||||
if (localStorage.getItem('htaccessNotificationDismissed') === 'true') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (localStorage.getItem('htaccessNotificationDismissed') === 'true') return;
|
||||
} catch (e) { return; }
|
||||
|
||||
// Check if notification has been shown today
|
||||
const lastShown = localStorage.getItem('htaccessNotificationLastShown');
|
||||
var lastShown;
|
||||
try { lastShown = localStorage.getItem('htaccessNotificationLastShown'); } catch (e) { return; }
|
||||
const today = new Date().toDateString();
|
||||
|
||||
if (lastShown === today) {
|
||||
@@ -2414,7 +2449,7 @@
|
||||
|
||||
// Show the notification
|
||||
showHtaccessNotification();
|
||||
localStorage.setItem('htaccessNotificationLastShown', today);
|
||||
try { localStorage.setItem('htaccessNotificationLastShown', today); } catch (e) {}
|
||||
}
|
||||
|
||||
function showHtaccessNotification() {
|
||||
@@ -2430,7 +2465,7 @@
|
||||
banner.classList.remove('show');
|
||||
body.classList.remove('htaccess-shown');
|
||||
// Remember dismissal permanently
|
||||
localStorage.setItem('htaccessNotificationDismissed', 'true');
|
||||
try { localStorage.setItem('htaccessNotificationDismissed', 'true'); } catch (e) {}
|
||||
}
|
||||
|
||||
function isNotificationDismissed(notificationKey) {
|
||||
@@ -2441,7 +2476,7 @@
|
||||
return {% if ai_scanner_notification_dismissed %}true{% else %}false{% endif %};
|
||||
}
|
||||
if (notificationKey === 'htaccess-notification') {
|
||||
return localStorage.getItem('htaccessNotificationDismissed') === 'true';
|
||||
try { return localStorage.getItem('htaccessNotificationDismissed') === 'true'; } catch (e) { return false; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -2516,6 +2551,10 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadNotificationCenter();
|
||||
checkBackupStatus();
|
||||
// Optional: open notification dropdown for testing (e.g. ?showNotifications=1)
|
||||
if (window.location.search.indexOf('showNotifications=1') !== -1) {
|
||||
setTimeout(function() { document.getElementById('notification-center-dropdown').classList.add('show'); }, 400);
|
||||
}
|
||||
// Show AI Scanner notification with a slight delay for better UX
|
||||
setTimeout(checkAIScannerStatus, 1000);
|
||||
// Show .htaccess notification with additional delay for staggered effect
|
||||
@@ -2560,10 +2599,12 @@
|
||||
|
||||
<!-- Dark Mode Toggle Script -->
|
||||
<script>
|
||||
// Theme switching functionality
|
||||
// Theme switching functionality (wrapped in try-catch for Tracking Prevention / private browsing)
|
||||
(function() {
|
||||
// Get saved theme from localStorage or default to light
|
||||
const savedTheme = localStorage.getItem('cyberPanelTheme') || 'light';
|
||||
var savedTheme = 'light';
|
||||
try {
|
||||
savedTheme = localStorage.getItem('cyberPanelTheme') || 'light';
|
||||
} catch (e) { /* Tracking Prevention or storage disabled */ }
|
||||
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||
|
||||
// Update icon based on current theme
|
||||
@@ -2586,7 +2627,7 @@
|
||||
|
||||
// Update theme
|
||||
document.documentElement.setAttribute('data-theme', newTheme);
|
||||
localStorage.setItem('cyberPanelTheme', newTheme);
|
||||
try { localStorage.setItem('cyberPanelTheme', newTheme); } catch (e) { /* Tracking Prevention */ }
|
||||
|
||||
// Update icon
|
||||
updateThemeIcon(newTheme);
|
||||
|
||||
@@ -199,9 +199,9 @@
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* Progress log */
|
||||
/* Progress log - dark background for terminal-style visibility */
|
||||
.log-container {
|
||||
background: var(--text-primary, #1e1e1e);
|
||||
background: #1e1e1e;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-top: 20px;
|
||||
@@ -215,11 +215,12 @@
|
||||
min-height: 300px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--bg-secondary, #f0f0f0);
|
||||
color: #e8e8e8;
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
resize: vertical;
|
||||
caret-color: #e8e8e8;
|
||||
}
|
||||
|
||||
.log-textarea:focus {
|
||||
@@ -337,30 +338,51 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Function to populate the branch dropdown
|
||||
function populateBranches(branches) {
|
||||
var branchSelect = document.getElementById("branchSelect");
|
||||
for (let i = branches.length - 1; i >= 0; i--) {
|
||||
const branch = branches[i];
|
||||
var option = document.createElement("option");
|
||||
option.value = branch;
|
||||
option.text = branch;
|
||||
if (branch.startsWith("v") && branch.indexOf("dev") === -1 && branch.indexOf("version-counter") === -1) {
|
||||
branchSelect.appendChild(option);
|
||||
}
|
||||
}
|
||||
// Sort key for version branches: v2.5.5-dev > v2.5.5 > v2.4.4
|
||||
function versionSortKey(name) {
|
||||
var m = name.match(/^v(\d+)\.(\d+)(?:\.(\d+))?(?:-(.+))?$/);
|
||||
if (!m) return [0, 0, 0, 0];
|
||||
var major = parseInt(m[1], 10) || 0;
|
||||
var minor = parseInt(m[2], 10) || 0;
|
||||
var patch = parseInt(m[3], 10) || 0;
|
||||
var suffix = (m[4] === 'dev') ? 1 : 0;
|
||||
return [major, minor, patch, suffix];
|
||||
}
|
||||
|
||||
function compareBranches(a, b) {
|
||||
var ka = versionSortKey(a), kb = versionSortKey(b);
|
||||
for (var i = 0; i < 4; i++) {
|
||||
if (ka[i] !== kb[i]) return kb[i] - ka[i];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Function to populate the branch dropdown (latest 10 only)
|
||||
function populateBranches(branches) {
|
||||
var filtered = branches.filter(function(b) {
|
||||
return b.startsWith("v") && b.indexOf("version-counter") === -1;
|
||||
});
|
||||
filtered.sort(compareBranches);
|
||||
var latest = filtered.slice(0, 10);
|
||||
|
||||
var branchSelect = document.getElementById("branchSelect");
|
||||
for (var i = 0; i < latest.length; i++) {
|
||||
var option = document.createElement("option");
|
||||
option.value = latest[i];
|
||||
option.text = latest[i];
|
||||
branchSelect.appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
function getBranches(url, branches, page) {
|
||||
if (!page) page = 1;
|
||||
fetch(url + '?page=' + page)
|
||||
fetch(url + '?per_page=100&page=' + page)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.length === 0) {
|
||||
populateBranches(branches);
|
||||
} else {
|
||||
const branchNames = data.map(branch => branch.name);
|
||||
var branchNames = data.map(function(b) { return b.name; });
|
||||
branches = branches.concat(branchNames);
|
||||
getBranches(url, branches, page + 1);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ urlpatterns = [
|
||||
re_path(r'^getAdminStatus$', views.getAdminStatus, name='getSystemInformation'),
|
||||
re_path(r'^getLoadAverage$', views.getLoadAverage, name='getLoadAverage'),
|
||||
re_path(r'^versionManagment$', views.versionManagment, name='versionManagment'),
|
||||
re_path(r'^versionManagement$', views.versionManagment, name='versionManagement'),
|
||||
re_path(r'^design$', views.design, name='design'),
|
||||
re_path(r'^getthemedata$', views.getthemedata, name='getthemedata'),
|
||||
re_path(r'^upgrade$', views.upgrade, name='upgrade'),
|
||||
|
||||
@@ -16,6 +16,7 @@ from plogical.acl import ACLManager
|
||||
from manageServices.models import PDNSStatus
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie, csrf_exempt
|
||||
from plogical.processUtilities import ProcessUtilities
|
||||
from plogical.firewallUtilities import FirewallUtilities
|
||||
from plogical.httpProc import httpProc
|
||||
from websiteFunctions.models import Websites, WPSites
|
||||
from databases.models import Databases
|
||||
@@ -25,6 +26,7 @@ from loginSystem.models import Administrator
|
||||
from packages.models import Package
|
||||
from django.views.decorators.http import require_GET, require_POST
|
||||
import pwd
|
||||
import re
|
||||
|
||||
# Create your views here.
|
||||
|
||||
@@ -32,6 +34,27 @@ VERSION = '2.5.5'
|
||||
BUILD = 'dev'
|
||||
|
||||
|
||||
def _version_compare(a, b):
|
||||
"""Return 1 if a > b, -1 if a < b, 0 if equal."""
|
||||
def parse(v):
|
||||
parts = []
|
||||
for p in str(v).split('.'):
|
||||
try:
|
||||
parts.append(int(p))
|
||||
except ValueError:
|
||||
parts.append(0)
|
||||
return parts
|
||||
pa, pb = parse(a), parse(b)
|
||||
for i in range(max(len(pa), len(pb))):
|
||||
va = pa[i] if i < len(pa) else 0
|
||||
vb = pb[i] if i < len(pb) else 0
|
||||
if va > vb:
|
||||
return 1
|
||||
if va < vb:
|
||||
return -1
|
||||
return 0
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def renderBase(request):
|
||||
template = 'baseTemplate/homePage.html'
|
||||
@@ -44,27 +67,41 @@ def renderBase(request):
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def versionManagement(request):
|
||||
currentVersion = VERSION
|
||||
currentBuild = str(BUILD)
|
||||
|
||||
getVersion = requests.get('https://cyberpanel.net/version.txt')
|
||||
latest = getVersion.json()
|
||||
latestVersion = latest['version']
|
||||
latestBuild = latest['build']
|
||||
branch_ref = 'v%s.%s' % (latestVersion, latestBuild)
|
||||
|
||||
currentVersion = VERSION
|
||||
currentBuild = str(BUILD)
|
||||
|
||||
u = "https://api.github.com/repos/usmannasir/cyberpanel/commits?sha=v%s.%s" % (latestVersion, latestBuild)
|
||||
logging.writeToFile(u)
|
||||
r = requests.get(u)
|
||||
latestcomit = r.json()[0]['sha']
|
||||
|
||||
command = "git -C /usr/local/CyberCP/ rev-parse HEAD"
|
||||
output = ProcessUtilities.outputExecutioner(command)
|
||||
|
||||
Currentcomt = output.rstrip("\n")
|
||||
notechk = True
|
||||
Currentcomt = ''
|
||||
latestcomit = ''
|
||||
|
||||
if Currentcomt == latestcomit:
|
||||
if _version_compare(currentVersion, latestVersion) > 0:
|
||||
notechk = False
|
||||
else:
|
||||
remote_cmd = 'git -C /usr/local/CyberCP remote get-url origin 2>/dev/null || true'
|
||||
remote_out = ProcessUtilities.outputExecutioner(remote_cmd)
|
||||
is_usmannasir = 'usmannasir/cyberpanel' in (remote_out or '')
|
||||
|
||||
if is_usmannasir:
|
||||
u = "https://api.github.com/repos/usmannasir/cyberpanel/commits?sha=%s" % branch_ref
|
||||
logging.CyberCPLogFileWriter.writeToFile(u)
|
||||
try:
|
||||
r = requests.get(u, timeout=10)
|
||||
r.raise_for_status()
|
||||
latestcomit = r.json()[0]['sha']
|
||||
except (requests.RequestException, IndexError, KeyError) as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile('[versionManagement] GitHub API failed: %s' % str(e))
|
||||
head_cmd = 'git -C /usr/local/CyberCP rev-parse HEAD 2>/dev/null || true'
|
||||
Currentcomt = ProcessUtilities.outputExecutioner(head_cmd).rstrip('\n')
|
||||
if latestcomit and Currentcomt == latestcomit:
|
||||
notechk = False
|
||||
else:
|
||||
notechk = False
|
||||
|
||||
template = 'baseTemplate/versionManagment.html'
|
||||
finalData = {'build': currentBuild, 'currentVersion': currentVersion, 'latestVersion': latestVersion,
|
||||
@@ -129,6 +166,11 @@ def getAdminStatus(request):
|
||||
|
||||
|
||||
def getSystemStatus(request):
|
||||
default_fallback = {
|
||||
'cpuUsage': 0, 'ramUsage': 0, 'diskUsage': 0,
|
||||
'cpuCores': 2, 'ramTotalMB': 4096, 'diskTotalGB': 100,
|
||||
'diskFreeGB': 100, 'uptime': 'N/A'
|
||||
}
|
||||
try:
|
||||
val = request.session['userID']
|
||||
currentACL = ACLManager.loadedACL(val)
|
||||
@@ -215,31 +257,13 @@ def getSystemStatus(request):
|
||||
|
||||
except KeyError as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'[getSystemStatus] KeyError - No session userID: {str(e)}')
|
||||
# Return default values on error
|
||||
default_data = {
|
||||
'cpuUsage': 0,
|
||||
'ramUsage': 0,
|
||||
'diskUsage': 0,
|
||||
'cpuCores': 2,
|
||||
'ramTotalMB': 4096,
|
||||
'diskTotalGB': 100,
|
||||
'diskFreeGB': 100,
|
||||
'uptime': 'N/A'
|
||||
}
|
||||
return HttpResponse(json.dumps(default_data))
|
||||
return HttpResponse(json.dumps(default_fallback))
|
||||
except Exception as e:
|
||||
# Return default values on error
|
||||
default_data = {
|
||||
'cpuUsage': 0,
|
||||
'ramUsage': 0,
|
||||
'diskUsage': 0,
|
||||
'cpuCores': 2,
|
||||
'ramTotalMB': 4096,
|
||||
'diskTotalGB': 100,
|
||||
'diskFreeGB': 100,
|
||||
'uptime': 'N/A'
|
||||
}
|
||||
return HttpResponse(json.dumps(default_data))
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'[getSystemStatus] Exception: {str(e)}')
|
||||
try:
|
||||
return HttpResponse(json.dumps(default_fallback))
|
||||
except Exception:
|
||||
return HttpResponse('{"cpuUsage":0,"ramUsage":0,"diskUsage":0,"cpuCores":2,"ramTotalMB":4096,"diskTotalGB":100,"diskFreeGB":100,"uptime":"N/A"}', content_type='application/json')
|
||||
|
||||
|
||||
def getLoadAverage(request):
|
||||
@@ -265,31 +289,75 @@ def getLoadAverage(request):
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def versionManagment(request):
|
||||
## Get latest version
|
||||
|
||||
getVersion = requests.get('https://cyberpanel.net/version.txt')
|
||||
latest = getVersion.json()
|
||||
latestVersion = latest['version']
|
||||
latestBuild = latest['build']
|
||||
|
||||
## Get local version
|
||||
|
||||
currentVersion = VERSION
|
||||
currentBuild = str(BUILD)
|
||||
|
||||
u = "https://api.github.com/repos/usmannasir/cyberpanel/commits?sha=v%s.%s" % (latestVersion, latestBuild)
|
||||
logging.CyberCPLogFileWriter.writeToFile(u)
|
||||
r = requests.get(u)
|
||||
latestcomit = r.json()[0]['sha']
|
||||
|
||||
command = "git -C /usr/local/CyberCP/ rev-parse HEAD"
|
||||
output = ProcessUtilities.outputExecutioner(command)
|
||||
|
||||
Currentcomt = output.rstrip("\n")
|
||||
notechk = True
|
||||
Currentcomt = ''
|
||||
latestcomit = ''
|
||||
latestVersion = '0'
|
||||
latestBuild = '0'
|
||||
|
||||
if (Currentcomt == latestcomit):
|
||||
on_dev_branch = (currentVersion == '2.5.5' and currentBuild == 'dev')
|
||||
|
||||
try:
|
||||
getVersion = requests.get('https://cyberpanel.net/version.txt', timeout=10)
|
||||
getVersion.raise_for_status()
|
||||
latest = getVersion.json()
|
||||
latestVersion = str(latest.get('version', '0'))
|
||||
latestBuild = str(latest.get('build', '0'))
|
||||
except (requests.RequestException, ValueError, KeyError) as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile('[versionManagment] cyberpanel.net/version.txt failed: %s' % str(e))
|
||||
if on_dev_branch:
|
||||
latestVersion, latestBuild = '2.5.5', 'dev'
|
||||
|
||||
# Dev branch: compare against v2.5.5-dev, show dev version info
|
||||
if on_dev_branch:
|
||||
branch_ref = 'v2.5.5-dev'
|
||||
latestVersion, latestBuild = '2.5.5', 'dev'
|
||||
else:
|
||||
branch_ref = 'v%s.%s' % (latestVersion, latestBuild)
|
||||
|
||||
# Always fetch local HEAD for display
|
||||
head_cmd = 'git -C /usr/local/CyberCP rev-parse HEAD 2>/dev/null || true'
|
||||
Currentcomt = (ProcessUtilities.outputExecutioner(head_cmd) or '').rstrip('\n')
|
||||
|
||||
remote_cmd = 'git -C /usr/local/CyberCP remote get-url origin 2>/dev/null || true'
|
||||
remote_out = ProcessUtilities.outputExecutioner(remote_cmd)
|
||||
is_usmannasir = 'usmannasir/cyberpanel' in (remote_out or '')
|
||||
|
||||
# Stable: newer than cyberpanel.net = up to date; dev: compare commits
|
||||
if not on_dev_branch and notechk and _version_compare(currentVersion, latestVersion) > 0:
|
||||
notechk = False
|
||||
elif notechk:
|
||||
# Dev branch: always use usmannasir v2.5.5-dev as canonical "latest"
|
||||
# Forks: use usmannasir for Latest Commit so all dev users compare to same upstream
|
||||
fetch_branch = branch_ref if (is_usmannasir or on_dev_branch) else None
|
||||
if fetch_branch:
|
||||
u = "https://api.github.com/repos/usmannasir/cyberpanel/commits?sha=%s" % fetch_branch
|
||||
logging.CyberCPLogFileWriter.writeToFile(u)
|
||||
try:
|
||||
r = requests.get(u, timeout=10)
|
||||
r.raise_for_status()
|
||||
latestcomit = r.json()[0]['sha']
|
||||
if Currentcomt and latestcomit and Currentcomt == latestcomit:
|
||||
notechk = False
|
||||
except (requests.RequestException, IndexError, KeyError) as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile('[versionManagment] GitHub API failed: %s' % str(e))
|
||||
elif not on_dev_branch:
|
||||
# Stable fork: fetch from fork's branch
|
||||
m = re.search(r'github\.com[:/]([^/]+)/([^/]+?)(?:\.git)?$', (remote_out or '').strip())
|
||||
if m:
|
||||
owner, repo = m.group(1), m.group(2).rstrip('.git')
|
||||
try:
|
||||
u = "https://api.github.com/repos/%s/%s/commits?sha=%s" % (owner, repo, branch_ref)
|
||||
r = requests.get(u, timeout=10)
|
||||
r.raise_for_status()
|
||||
latestcomit = r.json()[0]['sha']
|
||||
if Currentcomt and latestcomit and Currentcomt == latestcomit:
|
||||
notechk = False
|
||||
except (requests.RequestException, IndexError, KeyError):
|
||||
pass
|
||||
|
||||
template = 'baseTemplate/versionManagment.html'
|
||||
finalData = {'build': currentBuild, 'currentVersion': currentVersion, 'latestVersion': latestVersion,
|
||||
@@ -729,9 +797,19 @@ def getRecentSSHLogins(request):
|
||||
import re, time
|
||||
from collections import OrderedDict
|
||||
|
||||
# Run 'last -n 20' to get recent SSH logins
|
||||
# Pagination params
|
||||
try:
|
||||
output = ProcessUtilities.outputExecutioner('last -n 20')
|
||||
page = max(1, int(request.GET.get('page', 1)))
|
||||
except (ValueError, TypeError):
|
||||
page = 1
|
||||
try:
|
||||
per_page = min(100, max(5, int(request.GET.get('per_page', 20))))
|
||||
except (ValueError, TypeError):
|
||||
per_page = 20
|
||||
|
||||
# Run 'last -n 500' to get enough entries for pagination
|
||||
try:
|
||||
output = ProcessUtilities.outputExecutioner('last -n 500')
|
||||
except Exception as e:
|
||||
return HttpResponse(json.dumps({'error': 'Failed to run last: %s' % str(e)}), content_type='application/json', status=500)
|
||||
|
||||
@@ -815,7 +893,19 @@ def getRecentSSHLogins(request):
|
||||
'is_active': is_active,
|
||||
'raw': line
|
||||
})
|
||||
return HttpResponse(json.dumps({'logins': logins}), content_type='application/json')
|
||||
total = len(logins)
|
||||
total_pages = (total + per_page - 1) // per_page if total > 0 else 1
|
||||
page = min(page, total_pages) if total_pages > 0 else 1
|
||||
start = (page - 1) * per_page
|
||||
end = start + per_page
|
||||
paginated_logins = logins[start:end]
|
||||
return HttpResponse(json.dumps({
|
||||
'logins': paginated_logins,
|
||||
'total': total,
|
||||
'page': page,
|
||||
'per_page': per_page,
|
||||
'total_pages': total_pages
|
||||
}), content_type='application/json')
|
||||
except Exception as e:
|
||||
return HttpResponse(json.dumps({'error': str(e)}), content_type='application/json', status=500)
|
||||
|
||||
@@ -829,6 +919,17 @@ def getRecentSSHLogs(request):
|
||||
currentACL = ACLManager.loadedACL(user_id)
|
||||
if not currentACL.get('admin', 0):
|
||||
return HttpResponse(json.dumps({'error': 'Admin only'}), content_type='application/json', status=403)
|
||||
|
||||
# Pagination params
|
||||
try:
|
||||
page = max(1, int(request.GET.get('page', 1)))
|
||||
except (ValueError, TypeError):
|
||||
page = 1
|
||||
try:
|
||||
per_page = min(100, max(5, int(request.GET.get('per_page', 25))))
|
||||
except (ValueError, TypeError):
|
||||
per_page = 25
|
||||
|
||||
from plogical.processUtilities import ProcessUtilities
|
||||
import re
|
||||
distro = ProcessUtilities.decideDistro()
|
||||
@@ -837,7 +938,7 @@ def getRecentSSHLogs(request):
|
||||
else:
|
||||
log_path = '/var/log/secure'
|
||||
try:
|
||||
output = ProcessUtilities.outputExecutioner(f'tail -n 100 {log_path}')
|
||||
output = ProcessUtilities.outputExecutioner(f'tail -n 500 {log_path}')
|
||||
except Exception as e:
|
||||
return HttpResponse(json.dumps({'error': f'Failed to read log: {str(e)}'}), content_type='application/json', status=500)
|
||||
lines = output.split('\n')
|
||||
@@ -875,7 +976,21 @@ def getRecentSSHLogs(request):
|
||||
'raw': line,
|
||||
'ip_address': ip_address
|
||||
})
|
||||
return HttpResponse(json.dumps({'logs': logs}), content_type='application/json')
|
||||
# Reverse so newest logs appear first (page 1 = most recent)
|
||||
logs.reverse()
|
||||
total = len(logs)
|
||||
total_pages = (total + per_page - 1) // per_page if total > 0 else 1
|
||||
page = min(page, total_pages) if total_pages > 0 else 1
|
||||
start = (page - 1) * per_page
|
||||
end = start + per_page
|
||||
paginated_logs = logs[start:end]
|
||||
return HttpResponse(json.dumps({
|
||||
'logs': paginated_logs,
|
||||
'total': total,
|
||||
'page': page,
|
||||
'per_page': per_page,
|
||||
'total_pages': total_pages
|
||||
}), content_type='application/json')
|
||||
except Exception as e:
|
||||
return HttpResponse(json.dumps({'error': str(e)}), content_type='application/json', status=500)
|
||||
|
||||
@@ -1284,61 +1399,23 @@ def blockIPAddress(request):
|
||||
'error': 'Invalid IP address'
|
||||
}), content_type='application/json', status=400)
|
||||
|
||||
# Use firewalld (CSF has been discontinued)
|
||||
# Use FirewallUtilities so firewall-cmd runs with proper privileges (root/lscpd)
|
||||
firewall_cmd = 'firewalld'
|
||||
reason = data.get('reason', 'Security alert detected from dashboard')
|
||||
try:
|
||||
# Verify firewalld is active using subprocess for better security
|
||||
import subprocess
|
||||
firewalld_check = subprocess.run(['systemctl', 'is-active', 'firewalld'],
|
||||
capture_output=True, text=True, timeout=10)
|
||||
if not (firewalld_check.returncode == 0 and 'active' in firewalld_check.stdout):
|
||||
return HttpResponse(json.dumps({
|
||||
'status': 0,
|
||||
'error': 'Firewalld is not active. Please enable firewalld service.'
|
||||
}), content_type='application/json', status=500)
|
||||
except subprocess.TimeoutExpired:
|
||||
return HttpResponse(json.dumps({
|
||||
'status': 0,
|
||||
'error': 'Timeout checking firewalld status'
|
||||
}), content_type='application/json', status=500)
|
||||
success, msg = FirewallUtilities.blockIP(ip_address, reason)
|
||||
except Exception as e:
|
||||
return HttpResponse(json.dumps({
|
||||
'status': 0,
|
||||
'error': f'Cannot check firewalld status: {str(e)}'
|
||||
}), content_type='application/json', status=500)
|
||||
|
||||
# Block the IP address using firewalld with subprocess for better security
|
||||
success = False
|
||||
error_message = ''
|
||||
|
||||
try:
|
||||
# Use subprocess with explicit argument lists to prevent injection
|
||||
rich_rule = f'rule family=ipv4 source address={ip_address} drop'
|
||||
add_rule_cmd = ['firewall-cmd', '--permanent', '--add-rich-rule', rich_rule]
|
||||
|
||||
# Execute the add rule command
|
||||
result = subprocess.run(add_rule_cmd, capture_output=True, text=True, timeout=30)
|
||||
if result.returncode == 0:
|
||||
# Reload firewall rules
|
||||
reload_cmd = ['firewall-cmd', '--reload']
|
||||
reload_result = subprocess.run(reload_cmd, capture_output=True, text=True, timeout=30)
|
||||
if reload_result.returncode == 0:
|
||||
success = True
|
||||
else:
|
||||
error_message = f'Failed to reload firewall rules: {reload_result.stderr}'
|
||||
else:
|
||||
error_message = f'Failed to add firewall rule: {result.stderr}'
|
||||
except subprocess.TimeoutExpired:
|
||||
error_message = 'Firewall command timed out'
|
||||
except Exception as e:
|
||||
error_message = f'Firewall command failed: {str(e)}'
|
||||
success = False
|
||||
msg = str(e)
|
||||
|
||||
if success:
|
||||
# Add to banned IPs JSON file for consistency with firewall page
|
||||
try:
|
||||
import os
|
||||
import time
|
||||
banned_ips_file = '/etc/cyberpanel/banned_ips.json'
|
||||
primary_file = '/usr/local/CyberCP/data/banned_ips.json'
|
||||
legacy_file = '/etc/cyberpanel/banned_ips.json'
|
||||
banned_ips_file = primary_file if os.path.exists(primary_file) else legacy_file if os.path.exists(legacy_file) else primary_file
|
||||
banned_ips = []
|
||||
|
||||
if os.path.exists(banned_ips_file):
|
||||
@@ -1372,10 +1449,10 @@ def blockIPAddress(request):
|
||||
banned_ips.append(new_banned_ip)
|
||||
|
||||
# Ensure directory exists
|
||||
os.makedirs(os.path.dirname(banned_ips_file), exist_ok=True)
|
||||
os.makedirs(os.path.dirname(primary_file), exist_ok=True)
|
||||
|
||||
# Save to file
|
||||
with open(banned_ips_file, 'w') as f:
|
||||
with open(primary_file, 'w') as f:
|
||||
json.dump(banned_ips, f, indent=2)
|
||||
except Exception as e:
|
||||
# Log but don't fail the request if JSON update fails
|
||||
@@ -1394,7 +1471,7 @@ def blockIPAddress(request):
|
||||
else:
|
||||
return HttpResponse(json.dumps({
|
||||
'status': 0,
|
||||
'error': error_message or 'Failed to block IP address'
|
||||
'error': msg or 'Failed to block IP address'
|
||||
}), content_type='application/json', status=500)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
|
||||
2864
cyberpanel.sh
2864
cyberpanel.sh
File diff suppressed because it is too large
Load Diff
2945
cyberpanel_install_monolithic.sh
Normal file
2945
cyberpanel_install_monolithic.sh
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2266
cyberpanel_upgrade_monolithic.sh
Normal file
2266
cyberpanel_upgrade_monolithic.sh
Normal file
File diff suppressed because it is too large
Load Diff
@@ -21,7 +21,7 @@ check_OS() {
|
||||
Server_OS="AlmaLinux"
|
||||
elif grep -q -E "CloudLinux 7|CloudLinux 8" /etc/os-release ; then
|
||||
Server_OS="CloudLinux"
|
||||
elif grep -q -E "Ubuntu 18.04|Ubuntu 20.04|Ubuntu 20.10|Ubuntu 22.04" /etc/os-release ; then
|
||||
elif grep -q -E "Ubuntu 18.04|Ubuntu 20.04|Ubuntu 20.10|Ubuntu 22.04|Ubuntu 24.04" /etc/os-release ; then
|
||||
Server_OS="Ubuntu"
|
||||
elif grep -q -E "Rocky Linux" /etc/os-release ; then
|
||||
Server_OS="RockyLinux"
|
||||
@@ -29,7 +29,7 @@ check_OS() {
|
||||
Server_OS="openEuler"
|
||||
else
|
||||
echo -e "Unable to detect your system..."
|
||||
echo -e "\nCyberPanel is supported on x86_64 based Ubuntu 18.04, Ubuntu 20.04, Ubuntu 20.10, Ubuntu 22.04, CentOS 7, CentOS 8, AlmaLinux 8, AlmaLinux 9, AlmaLinux 10, RockyLinux 8, CloudLinux 7, CloudLinux 8, openEuler 20.03, openEuler 22.03...\n"
|
||||
echo -e "\nCyberPanel is supported on x86_64 based Ubuntu 18.04, Ubuntu 20.04, Ubuntu 20.10, Ubuntu 22.04, Ubuntu 24.04, CentOS 7, CentOS 8, AlmaLinux 8, AlmaLinux 9, AlmaLinux 10, RockyLinux 8, CloudLinux 7, CloudLinux 8, openEuler 20.03, openEuler 22.03...\n"
|
||||
exit
|
||||
fi
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ class DatabaseManager:
|
||||
|
||||
def Upgardemysql(self, request = None, userID = None):
|
||||
data={}
|
||||
data['mysqlversions']=['10.6','10.11']
|
||||
data['mysqlversions']=['10.6','10.11','11.8']
|
||||
template = 'databases/Updatemysql.html'
|
||||
proc = httpProc(request, template, data, 'admin')
|
||||
return proc.render()
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
* Created by usman on 8/6/17.
|
||||
*/
|
||||
|
||||
/* Ensure we register controllers on the CyberCP app (avoids ctrlreg if load order differs) */
|
||||
var app = (typeof window.app !== 'undefined' && window.app) ? window.app : angular.module('CyberCP');
|
||||
|
||||
/* Java script code to create database */
|
||||
app.controller('createDatabase', function ($scope, $http) {
|
||||
@@ -683,14 +685,18 @@ app.controller('phpMyAdmin', function ($scope, $http, $window) {
|
||||
|
||||
app.controller('Mysqlmanager', function ($scope, $http, $compile, $window, $timeout) {
|
||||
$scope.cyberPanelLoading = false;
|
||||
$scope.mysql_status = 'test'
|
||||
$scope.mysql_status = 'test';
|
||||
$scope.uptime = '—';
|
||||
$scope.connections = '—';
|
||||
$scope.Slow_queries = '—';
|
||||
$scope.processes = [];
|
||||
|
||||
|
||||
$scope.getstatus = function () {
|
||||
|
||||
$scope.cyberPanelLoading = true;
|
||||
|
||||
url = "/dataBases/getMysqlstatus";
|
||||
url = "/dataBases/getMysqlstatus?t=" + (Date.now ? Date.now() : new Date().getTime());
|
||||
|
||||
var data = {};
|
||||
|
||||
@@ -706,12 +712,29 @@ app.controller('Mysqlmanager', function ($scope, $http, $compile, $window, $time
|
||||
|
||||
function ListInitialDatas(response) {
|
||||
$scope.cyberPanelLoading = false;
|
||||
if (response.data.status === 1) {
|
||||
$scope.uptime = response.data.uptime;
|
||||
$scope.connections = response.data.connections;
|
||||
$scope.Slow_queries = response.data.Slow_queries;
|
||||
$scope.processes = JSON.parse(response.data.processes);
|
||||
$timeout($scope.showStatus, 3000);
|
||||
var data = response.data;
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch (e) {
|
||||
$scope.uptime = $scope.connections = $scope.Slow_queries = '—';
|
||||
$scope.processes = [];
|
||||
new PNotify({ title: 'Error!', text: 'Invalid response from server.', type: 'error' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (data && data.status === 1) {
|
||||
$scope.uptime = data.uptime || '—';
|
||||
$scope.connections = data.connections != null ? data.connections : '—';
|
||||
$scope.Slow_queries = data.Slow_queries != null ? data.Slow_queries : '—';
|
||||
try {
|
||||
$scope.processes = typeof data.processes === 'string' ? JSON.parse(data.processes || '[]') : (data.processes || []);
|
||||
} catch (e) {
|
||||
$scope.processes = [];
|
||||
}
|
||||
if (typeof $scope.showStatus === 'function') {
|
||||
$timeout($scope.showStatus, 3000);
|
||||
}
|
||||
|
||||
new PNotify({
|
||||
title: 'Success',
|
||||
@@ -721,7 +744,7 @@ app.controller('Mysqlmanager', function ($scope, $http, $compile, $window, $time
|
||||
} else {
|
||||
new PNotify({
|
||||
title: 'Error!',
|
||||
text: response.data.error_message,
|
||||
text: (data && data.error_message) || 'Could not load MySQL status.',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
@@ -730,15 +753,39 @@ app.controller('Mysqlmanager', function ($scope, $http, $compile, $window, $time
|
||||
|
||||
function cantLoadInitialDatas(response) {
|
||||
$scope.cyberPanelLoading = false;
|
||||
$scope.uptime = '—';
|
||||
$scope.connections = '—';
|
||||
$scope.Slow_queries = '—';
|
||||
$scope.processes = [];
|
||||
var msg = 'Cannot load MySQL status.';
|
||||
if (response && response.status) {
|
||||
msg = 'Request failed: ' + response.status + (response.statusText ? ' ' + response.statusText : '');
|
||||
}
|
||||
if (response && response.data) {
|
||||
if (typeof response.data === 'string' && response.data.length < 200) {
|
||||
msg = response.data;
|
||||
} else if (response.data.error_message) {
|
||||
msg = response.data.error_message;
|
||||
}
|
||||
}
|
||||
new PNotify({
|
||||
title: 'Error!',
|
||||
text: "cannot load",
|
||||
text: msg,
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$scope.refreshProcesses = function () {
|
||||
var icon = document.querySelector('.refresh-btn i');
|
||||
if (icon) {
|
||||
icon.style.animation = 'spin 1s linear';
|
||||
setTimeout(function () { icon.style.animation = ''; }, 1000);
|
||||
}
|
||||
$scope.getstatus();
|
||||
};
|
||||
|
||||
$scope.getstatus();
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
<span style="display: none" id="password">{{ password }}</span>
|
||||
<form style="display: none" name="loginform" id="loginform" action="/phpmyadmin/phpmyadminsignin.php" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="host" value="{{ host|default:'127.0.0.1' }}"/>
|
||||
<input type="hidden" name="port" value="{{ port|default:'3306' }}"/>
|
||||
<p>
|
||||
<label for="user_login">Username or Email Address</label>
|
||||
<input type="text" name="username" id="user_login" class="input" value="" size="20" autocapitalize="off"/>
|
||||
|
||||
@@ -644,8 +644,8 @@
|
||||
</td>
|
||||
<td>
|
||||
<span class="info-text"
|
||||
ng-bind="process.info || 'No query'"
|
||||
ng-attr-title="{$ process.info $}"></span>
|
||||
ng-bind="(process.info && process.info !== 'NULL') ? process.info : 'No query'"
|
||||
ng-attr-title="{$ (process.info && process.info !== 'NULL') ? process.info : 'No query' $}"></span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="progress-bar">
|
||||
@@ -667,23 +667,4 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Add refresh functionality if not already present
|
||||
if (typeof app !== 'undefined') {
|
||||
app.controller('Mysqlmanager', function($scope, $http, $interval) {
|
||||
// Existing controller code...
|
||||
|
||||
$scope.refreshProcesses = function() {
|
||||
// Trigger a refresh of the processes
|
||||
var icon = document.querySelector('.refresh-btn i');
|
||||
icon.style.animation = 'spin 1s linear';
|
||||
setTimeout(function() {
|
||||
icon.style.animation = '';
|
||||
}, 1000);
|
||||
// The actual refresh logic should be implemented in the controller
|
||||
};
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -294,6 +294,11 @@ def fetchDetailsPHPMYAdmin(request):
|
||||
data = {}
|
||||
data['userName'] = mysqluser
|
||||
data['password'] = password
|
||||
# Use 127.0.0.1 so phpMyAdmin connects via TCP (port 3306), same as main MariaDB
|
||||
data['host'] = jsonData.get('mysqlhost', '127.0.0.1') or '127.0.0.1'
|
||||
if data['host'] == 'localhost':
|
||||
data['host'] = '127.0.0.1'
|
||||
data['port'] = str(jsonData.get('mysqlport', 3306))
|
||||
|
||||
proc = httpProc(request, 'databases/AutoLogin.html',
|
||||
data, 'admin')
|
||||
@@ -309,6 +314,8 @@ def fetchDetailsPHPMYAdmin(request):
|
||||
data = {}
|
||||
data['userName'] = 'root'
|
||||
data['password'] = password
|
||||
data['host'] = '127.0.0.1'
|
||||
data['port'] = '3306'
|
||||
# return redirect(returnURL)
|
||||
|
||||
proc = httpProc(request, 'databases/AutoLogin.html',
|
||||
@@ -333,6 +340,8 @@ def fetchDetailsPHPMYAdmin(request):
|
||||
data = {}
|
||||
data['userName'] = admin.userName
|
||||
data['password'] = password.decode()
|
||||
data['host'] = '127.0.0.1'
|
||||
data['port'] = '3306'
|
||||
# return redirect(returnURL)
|
||||
|
||||
proc = httpProc(request, 'databases/AutoLogin.html',
|
||||
@@ -381,17 +390,18 @@ def UpgradeMySQL(request):
|
||||
def getMysqlstatus(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
finalData = mysqlUtilities.showStatus()
|
||||
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
|
||||
if currentACL['admin'] == 1:
|
||||
pass
|
||||
else:
|
||||
if currentACL['admin'] != 1:
|
||||
return ACLManager.loadErrorJson('FilemanagerAdmin', 0)
|
||||
|
||||
finalData = json.dumps(finalData)
|
||||
return HttpResponse(finalData)
|
||||
finalData = mysqlUtilities.showStatus()
|
||||
if finalData == 0:
|
||||
finalData = {'status': 0, 'error_message': 'Could not connect to MySQL or fetch status.'}
|
||||
else:
|
||||
finalData.setdefault('status', 1)
|
||||
body = json.dumps(finalData)
|
||||
return HttpResponse(body, content_type='application/json')
|
||||
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
47
deploy-createuser-fix.sh
Executable file
47
deploy-createuser-fix.sh
Executable file
@@ -0,0 +1,47 @@
|
||||
#!/bin/bash
|
||||
# Deploy Create User page fix (userCreationFailed / "Unknown error" on load)
|
||||
# Run this on the CyberPanel server (e.g. after pulling repo or copying fixed files)
|
||||
# Usage: sudo bash deploy-createuser-fix.sh
|
||||
|
||||
set -e
|
||||
|
||||
CYBERCP_ROOT="${CYBERCP_ROOT:-/usr/local/CyberCP}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PYTHON="${CYBERCP_ROOT}/bin/python"
|
||||
if [ ! -x "$PYTHON" ]; then
|
||||
PYTHON="python3"
|
||||
fi
|
||||
|
||||
echo "[$(date +%Y-%m-%d\ %H:%M:%S)] Deploying Create User fix..."
|
||||
|
||||
# If running from repo (this workspace), copy fixed JS into CyberCP
|
||||
if [ -f "$SCRIPT_DIR/userManagment/static/userManagment/userManagment.js" ]; then
|
||||
echo " Copying userManagment.js from repo app static..."
|
||||
mkdir -p "$CYBERCP_ROOT/userManagment/static/userManagment"
|
||||
cp -f "$SCRIPT_DIR/userManagment/static/userManagment/userManagment.js" \
|
||||
"$CYBERCP_ROOT/userManagment/static/userManagment/userManagment.js"
|
||||
fi
|
||||
if [ -f "$SCRIPT_DIR/static/userManagment/userManagment.js" ]; then
|
||||
echo " Copying userManagment.js from repo static..."
|
||||
mkdir -p "$CYBERCP_ROOT/static/userManagment"
|
||||
cp -f "$SCRIPT_DIR/static/userManagment/userManagment.js" \
|
||||
"$CYBERCP_ROOT/static/userManagment/userManagment.js"
|
||||
fi
|
||||
if [ -f "$SCRIPT_DIR/public/static/userManagment/userManagment.js" ]; then
|
||||
echo " Copying userManagment.js from repo public/static..."
|
||||
mkdir -p "$CYBERCP_ROOT/public/static/userManagment"
|
||||
cp -f "$SCRIPT_DIR/public/static/userManagment/userManagment.js" \
|
||||
"$CYBERCP_ROOT/public/static/userManagment/userManagment.js"
|
||||
fi
|
||||
|
||||
# Run collectstatic so served static gets the fix
|
||||
if [ -f "$CYBERCP_ROOT/manage.py" ]; then
|
||||
echo " Running collectstatic..."
|
||||
cd "$CYBERCP_ROOT"
|
||||
"$PYTHON" manage.py collectstatic --noinput --clear 2>&1 | tail -5
|
||||
echo " collectstatic done."
|
||||
else
|
||||
echo " No $CYBERCP_ROOT/manage.py found; skipping collectstatic."
|
||||
fi
|
||||
|
||||
echo "[$(date +%Y-%m-%d\ %H:%M:%S)] Create User fix deployed. Hard-refresh browser (Ctrl+F5) on the Create User page."
|
||||
78
deploy-email-limits-fix.sh
Executable file
78
deploy-email-limits-fix.sh
Executable file
@@ -0,0 +1,78 @@
|
||||
#!/bin/bash
|
||||
# Deploy Email Limits fix to a CyberPanel installation.
|
||||
# Copies recommended mailServer files and optionally restarts lscpd.
|
||||
#
|
||||
# Usage (run from anywhere):
|
||||
# sudo bash /home/cyberpanel-repo/deploy-email-limits-fix.sh
|
||||
# sudo bash deploy-email-limits-fix.sh [REPO_DIR] [CP_DIR]
|
||||
#
|
||||
# Or from repo root: cd /home/cyberpanel-repo && sudo bash deploy-email-limits-fix.sh
|
||||
|
||||
set -e
|
||||
|
||||
log() { echo "[$(date +%Y-%m-%d\ %H:%M:%S)] $*"; }
|
||||
err() { log "ERROR: $*" >&2; }
|
||||
|
||||
# Resolve REPO_DIR: explicit arg, then script dir, then common locations
|
||||
if [[ -n "$1" && -d "$1/mailServer" ]]; then
|
||||
REPO_DIR="$1"
|
||||
shift
|
||||
elif [[ -d "$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)/mailServer" ]]; then
|
||||
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
elif [[ -d "/home/cyberpanel-repo/mailServer" ]]; then
|
||||
REPO_DIR="/home/cyberpanel-repo"
|
||||
elif [[ -d "./mailServer" ]]; then
|
||||
REPO_DIR="$(pwd)"
|
||||
else
|
||||
err "Repo not found. Use: sudo bash /home/cyberpanel-repo/deploy-email-limits-fix.sh"
|
||||
err "Or: cd /path/to/cyberpanel-repo && sudo bash deploy-email-limits-fix.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CP_DIR="${1:-/usr/local/CyberCP}"
|
||||
RESTART_LSCPD="${RESTART_LSCPD:-1}"
|
||||
|
||||
if [[ ! -d "$CP_DIR" ]]; then
|
||||
err "CyberPanel directory not found: $CP_DIR"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -d "$REPO_DIR/mailServer" ]]; then
|
||||
err "Repo mailServer not found in: $REPO_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "REPO_DIR=$REPO_DIR"
|
||||
log "CP_DIR=$CP_DIR"
|
||||
|
||||
FILES=(
|
||||
"mailServer/mailserverManager.py"
|
||||
"mailServer/templates/mailServer/EmailLimits.html"
|
||||
"mailServer/static/mailServer/mailServer.js"
|
||||
"mailServer/static/mailServer/emailLimitsController.js"
|
||||
)
|
||||
|
||||
for rel in "${FILES[@]}"; do
|
||||
src="$REPO_DIR/$rel"
|
||||
dst="$CP_DIR/$rel"
|
||||
if [[ ! -f "$src" ]]; then
|
||||
err "Source missing: $src"
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p "$(dirname "$dst")"
|
||||
cp -f "$src" "$dst"
|
||||
log "Copied: $rel"
|
||||
done
|
||||
|
||||
if [[ "$RESTART_LSCPD" =~ ^(1|yes|true)$ ]]; then
|
||||
if systemctl is-active --quiet lscpd 2>/dev/null; then
|
||||
log "Restarting lscpd..."
|
||||
systemctl restart lscpd || { err "lscpd restart failed"; exit 1; }
|
||||
log "lscpd restarted."
|
||||
else
|
||||
log "lscpd not running or not a systemd service; skip restart."
|
||||
fi
|
||||
else
|
||||
log "Skipping restart (set RESTART_LSCPD=1 to restart lscpd)."
|
||||
fi
|
||||
|
||||
log "Deploy complete. Hard-refresh /email/EmailLimits in the browser (Ctrl+Shift+R)."
|
||||
64
deploy-ftp-create-account-fix.sh
Normal file
64
deploy-ftp-create-account-fix.sh
Normal file
@@ -0,0 +1,64 @@
|
||||
#!/bin/bash
|
||||
# Deploy FTP Create Account template fix to a CyberPanel installation.
|
||||
# Copies the updated createFTPAccount.html and optionally restarts lscpd.
|
||||
#
|
||||
# Usage (run from anywhere):
|
||||
# sudo bash /home/cyberpanel-repo/deploy-ftp-create-account-fix.sh
|
||||
# sudo bash deploy-ftp-create-account-fix.sh [REPO_DIR] [CP_DIR]
|
||||
#
|
||||
# Or from repo root: cd /home/cyberpanel-repo && sudo bash deploy-ftp-create-account-fix.sh
|
||||
|
||||
set -e
|
||||
|
||||
log() { echo "[$(date +%Y-%m-%d\ %H:%M:%S)] $*"; }
|
||||
err() { log "ERROR: $*" >&2; }
|
||||
|
||||
# Resolve REPO_DIR
|
||||
if [[ -n "$1" && -d "$1/ftp" ]]; then
|
||||
REPO_DIR="$1"
|
||||
shift
|
||||
elif [[ -d "$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)/ftp" ]]; then
|
||||
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
elif [[ -d "/home/cyberpanel-repo/ftp" ]]; then
|
||||
REPO_DIR="/home/cyberpanel-repo"
|
||||
elif [[ -d "./ftp" ]]; then
|
||||
REPO_DIR="$(pwd)"
|
||||
else
|
||||
err "Repo not found. Use: sudo bash /home/cyberpanel-repo/deploy-ftp-create-account-fix.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CP_DIR="${1:-/usr/local/CyberCP}"
|
||||
RESTART_LSCPD="${RESTART_LSCPD:-1}"
|
||||
|
||||
if [[ ! -d "$CP_DIR" ]]; then
|
||||
err "CyberPanel directory not found: $CP_DIR"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$REPO_DIR/ftp/templates/ftp/createFTPAccount.html" ]]; then
|
||||
err "Source template not found in: $REPO_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "REPO_DIR=$REPO_DIR"
|
||||
log "CP_DIR=$CP_DIR"
|
||||
|
||||
SRC="$REPO_DIR/ftp/templates/ftp/createFTPAccount.html"
|
||||
DST="$CP_DIR/ftp/templates/ftp/createFTPAccount.html"
|
||||
mkdir -p "$(dirname "$DST")"
|
||||
cp -f "$SRC" "$DST"
|
||||
log "Copied: ftp/templates/ftp/createFTPAccount.html"
|
||||
|
||||
if [[ "$RESTART_LSCPD" =~ ^(1|yes|true)$ ]]; then
|
||||
if systemctl is-active --quiet lscpd 2>/dev/null; then
|
||||
log "Restarting lscpd..."
|
||||
systemctl restart lscpd || { err "lscpd restart failed"; exit 1; }
|
||||
log "lscpd restarted."
|
||||
else
|
||||
log "lscpd not running or not a systemd service; skip restart."
|
||||
fi
|
||||
else
|
||||
log "Skipping restart (set RESTART_LSCPD=1 to restart lscpd)."
|
||||
fi
|
||||
|
||||
log "Deploy complete. Hard-refresh /ftp/createFTPAccount in the browser (Ctrl+Shift+R)."
|
||||
65
deploy-ftp-quotas-table.sh
Normal file
65
deploy-ftp-quotas-table.sh
Normal file
@@ -0,0 +1,65 @@
|
||||
#!/bin/bash
|
||||
# Create the missing ftp_quotas table in the CyberPanel database.
|
||||
# Fixes: (1146, "Table 'cyberpanel.ftp_quotas' doesn't exist") on /ftp/quotaManagement
|
||||
#
|
||||
# Usage:
|
||||
# sudo bash /home/cyberpanel-repo/deploy-ftp-quotas-table.sh
|
||||
# sudo bash deploy-ftp-quotas-table.sh [REPO_DIR] [CP_DIR]
|
||||
|
||||
set -e
|
||||
|
||||
log() { echo "[$(date +%Y-%m-%d\ %H:%M:%S)] $*"; }
|
||||
err() { log "ERROR: $*" >&2; }
|
||||
|
||||
if [[ -n "$1" && -f "$1/sql/create_ftp_quotas.sql" ]]; then
|
||||
REPO_DIR="$1"
|
||||
shift
|
||||
elif [[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)/sql/create_ftp_quotas.sql" ]]; then
|
||||
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
elif [[ -f "/home/cyberpanel-repo/sql/create_ftp_quotas.sql" ]]; then
|
||||
REPO_DIR="/home/cyberpanel-repo"
|
||||
else
|
||||
err "sql/create_ftp_quotas.sql not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CP_DIR="${1:-/usr/local/CyberCP}"
|
||||
SQL_FILE="$REPO_DIR/sql/create_ftp_quotas.sql"
|
||||
|
||||
if [[ ! -d "$CP_DIR" ]]; then
|
||||
err "CyberPanel directory not found: $CP_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "REPO_DIR=$REPO_DIR"
|
||||
log "CP_DIR=$CP_DIR"
|
||||
|
||||
mkdir -p "$CP_DIR/sql"
|
||||
cp -f "$SQL_FILE" "$CP_DIR/sql/create_ftp_quotas.sql"
|
||||
log "Copied create_ftp_quotas.sql to $CP_DIR/sql/"
|
||||
|
||||
# Run SQL using Django DB connection (no password on command line)
|
||||
log "Creating ftp_quotas table..."
|
||||
export CP_DIR
|
||||
python3 << 'PYEOF'
|
||||
import os
|
||||
import sys
|
||||
|
||||
cp_dir = os.environ.get('CP_DIR', '/usr/local/CyberCP')
|
||||
sys.path.insert(0, cp_dir)
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'CyberCP.settings')
|
||||
|
||||
import django
|
||||
django.setup()
|
||||
|
||||
from django.db import connection
|
||||
|
||||
with open(os.path.join(cp_dir, 'sql', 'create_ftp_quotas.sql'), 'r') as f:
|
||||
sql = f.read()
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(sql)
|
||||
print('Executed CREATE TABLE IF NOT EXISTS ftp_quotas.')
|
||||
PYEOF
|
||||
|
||||
log "Done. Reload https://207.180.193.210:2087/ftp/quotaManagement"
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# CyberPanel phpMyAdmin Access Control Deployment Script
|
||||
# This script implements redirect functionality for unauthenticated phpMyAdmin access
|
||||
|
||||
echo "=== CyberPanel phpMyAdmin Access Control Deployment ==="
|
||||
|
||||
# Check if running as root
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "Please run this script as root"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Backup original phpMyAdmin index.php if it exists
|
||||
if [ -f "/usr/local/CyberCP/public/phpmyadmin/index.php" ]; then
|
||||
echo "Backing up original phpMyAdmin index.php..."
|
||||
cp /usr/local/CyberCP/public/phpmyadmin/index.php /usr/local/CyberCP/public/phpmyadmin/index.php.backup.$(date +%Y%m%d_%H%M%S)
|
||||
fi
|
||||
|
||||
# Deploy the redirect index.php
|
||||
echo "Deploying phpMyAdmin access control..."
|
||||
cp /usr/local/CyberCP/phpmyadmin_index_redirect.php /usr/local/CyberCP/public/phpmyadmin/index.php
|
||||
|
||||
# Deploy .htaccess for additional protection
|
||||
echo "Deploying .htaccess protection..."
|
||||
cp /usr/local/CyberCP/phpmyadmin_htaccess /usr/local/CyberCP/public/phpmyadmin/.htaccess
|
||||
|
||||
# Set proper permissions
|
||||
echo "Setting permissions..."
|
||||
chown lscpd:lscpd /usr/local/CyberCP/public/phpmyadmin/index.php
|
||||
chmod 644 /usr/local/CyberCP/public/phpmyadmin/index.php
|
||||
chown lscpd:lscpd /usr/local/CyberCP/public/phpmyadmin/.htaccess
|
||||
chmod 644 /usr/local/CyberCP/public/phpmyadmin/.htaccess
|
||||
|
||||
# Restart LiteSpeed to ensure changes take effect
|
||||
echo "Restarting LiteSpeed..."
|
||||
systemctl restart lscpd
|
||||
|
||||
echo "=== Deployment Complete ==="
|
||||
echo ""
|
||||
echo "phpMyAdmin access control has been deployed successfully!"
|
||||
echo ""
|
||||
echo "What this does:"
|
||||
echo "- Users trying to access phpMyAdmin directly without being logged into CyberPanel"
|
||||
echo " will now be redirected to the CyberPanel login page (/base/)"
|
||||
echo "- Authenticated users will continue to access phpMyAdmin normally"
|
||||
echo ""
|
||||
echo "To revert changes, restore the backup:"
|
||||
echo "cp /usr/local/CyberCP/public/phpmyadmin/index.php.backup.* /usr/local/CyberCP/public/phpmyadmin/index.php"
|
||||
echo ""
|
||||
echo "Test the implementation by:"
|
||||
echo "1. Opening an incognito/private browser window"
|
||||
echo "2. Going to https://your-server:2087/phpmyadmin/"
|
||||
echo "3. You should be redirected to the CyberPanel login page"
|
||||
@@ -775,11 +775,14 @@ class DNSManager:
|
||||
else:
|
||||
ttl = dns_record['ttl']
|
||||
|
||||
prio = dns_record.get('priority', 0)
|
||||
if prio is None:
|
||||
prio = 0
|
||||
dic = {'id': dns_record['id'],
|
||||
'type': dns_record['type'],
|
||||
'name': dns_record['name'],
|
||||
'content': dns_record['content'],
|
||||
'priority': '1400',
|
||||
'priority': str(prio),
|
||||
'ttl': ttl,
|
||||
'proxy': dns_record['proxied'],
|
||||
'proxiable': dns_record['proxiable']
|
||||
@@ -844,6 +847,67 @@ class DNSManager:
|
||||
final_json = json.dumps(final_dic)
|
||||
return HttpResponse(final_json)
|
||||
|
||||
def updateDNSRecordCloudFlare(self, userID=None, data=None):
|
||||
"""Update an existing CloudFlare DNS record (name, type, ttl, content, priority, proxied)."""
|
||||
try:
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
if ACLManager.currentContextPermission(currentACL, 'addDeleteRecords') == 0:
|
||||
return ACLManager.loadErrorJson('update_status', 0)
|
||||
|
||||
zone_domain = data['selectedZone']
|
||||
record_id = data['id']
|
||||
name = (data.get('name') or '').strip()
|
||||
record_type = (data.get('recordType') or data.get('type') or '').strip()
|
||||
content = (data.get('content') or '').strip()
|
||||
ttl_val = data.get('ttl')
|
||||
priority = data.get('priority', 0)
|
||||
proxied = data.get('proxied', False)
|
||||
|
||||
if not name or not record_type or not content:
|
||||
final_json = json.dumps({'status': 0, 'update_status': 0, 'error_message': 'Name, type and content are required.'})
|
||||
return HttpResponse(final_json)
|
||||
|
||||
try:
|
||||
ttl_int = int(ttl_val) if ttl_val not in (None, '', 'AUTO') else 1
|
||||
except (ValueError, TypeError):
|
||||
ttl_int = 1
|
||||
if ttl_int < 0:
|
||||
ttl_int = 1
|
||||
elif ttl_int > 86400 and ttl_int != 1:
|
||||
ttl_int = 86400
|
||||
|
||||
try:
|
||||
priority_int = int(priority) if priority not in (None, '') else 0
|
||||
except (ValueError, TypeError):
|
||||
priority_int = 0
|
||||
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
self.admin = admin
|
||||
if ACLManager.checkOwnershipZone(zone_domain, admin, currentACL) != 1:
|
||||
return ACLManager.loadErrorJson()
|
||||
|
||||
self.loadCFKeys()
|
||||
params = {'name': zone_domain, 'per_page': 50}
|
||||
cf = CloudFlare.CloudFlare(email=self.email, token=self.key)
|
||||
zones = cf.zones.get(params=params)
|
||||
zone_list = sorted(zones, key=lambda v: v['name'])
|
||||
if not zone_list:
|
||||
final_json = json.dumps({'status': 0, 'update_status': 0, 'error_message': 'Zone not found.'})
|
||||
return HttpResponse(final_json)
|
||||
zone_id = zone_list[0]['id']
|
||||
|
||||
update_data = {'name': name, 'type': record_type, 'content': content, 'ttl': ttl_int, 'priority': priority_int}
|
||||
if record_type in ['A', 'CNAME']:
|
||||
update_data['proxied'] = bool(proxied)
|
||||
|
||||
cf.zones.dns_records.put(zone_id, record_id, data=update_data)
|
||||
final_dic = {'status': 1, 'update_status': 1, 'error_message': 'None'}
|
||||
final_json = json.dumps(final_dic)
|
||||
return HttpResponse(final_json)
|
||||
except BaseException as msg:
|
||||
final_dic = {'status': 0, 'update_status': 0, 'error_message': str(msg)}
|
||||
final_json = json.dumps(final_dic)
|
||||
return HttpResponse(final_json)
|
||||
|
||||
def addDNSRecordCloudFlare(self, userID = None, data = None):
|
||||
try:
|
||||
@@ -1099,22 +1163,17 @@ class DNSManager:
|
||||
params = {'name': zoneDomain, 'per_page': 50}
|
||||
cf = CloudFlare.CloudFlare(email=self.email, token=self.key)
|
||||
|
||||
## Get zone
|
||||
|
||||
zones = cf.zones.get(params=params)
|
||||
if not zones:
|
||||
final_dic = {'status': 0, 'delete_status': 0, 'error_message': 'Zone not found'}
|
||||
return HttpResponse(json.dumps(final_dic), status=400)
|
||||
|
||||
zone = zones[0]
|
||||
|
||||
##
|
||||
|
||||
zone_id = zone['id']
|
||||
|
||||
params = {'name': name}
|
||||
dns_records = cf.zones.dns_records.get(zone_id, params=params)
|
||||
|
||||
##
|
||||
|
||||
|
||||
if value == True:
|
||||
new_r_proxied_flag = False
|
||||
else:
|
||||
@@ -1128,11 +1187,9 @@ class DNSManager:
|
||||
r_proxied = dns_record['proxied']
|
||||
|
||||
if r_proxied == new_r_proxied_flag:
|
||||
# Nothing to do
|
||||
continue
|
||||
|
||||
dns_record_id = dns_record['id']
|
||||
|
||||
new_dns_record = {
|
||||
'type': r_type,
|
||||
'name': r_name,
|
||||
@@ -1140,12 +1197,13 @@ class DNSManager:
|
||||
'ttl': r_ttl,
|
||||
'proxied': new_r_proxied_flag
|
||||
}
|
||||
|
||||
cf.zones.dns_records.put(zone_id, dns_record_id, data=new_dns_record)
|
||||
|
||||
final_dic = {'status': 1, 'delete_status': 1, 'error_message': "None"}
|
||||
final_json = json.dumps(final_dic)
|
||||
return HttpResponse(final_json)
|
||||
return HttpResponse(json.dumps(final_dic))
|
||||
|
||||
final_dic = {'status': 1, 'delete_status': 1, 'error_message': "None"}
|
||||
return HttpResponse(json.dumps(final_dic))
|
||||
|
||||
except BaseException as msg:
|
||||
final_dic = {'status': 0, 'delete_status': 0, 'error_message': str(msg)}
|
||||
|
||||
@@ -203,6 +203,27 @@
|
||||
background: var(--bg-secondary, #f8f9ff);
|
||||
border-color: #5b5fcf;
|
||||
}
|
||||
|
||||
.edit-record-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
.edit-record-modal {
|
||||
background: var(--bg-primary, white);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
max-width: 480px;
|
||||
width: 90%;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 1rem 1.5rem;
|
||||
@@ -527,6 +548,84 @@
|
||||
outline: 2px solid rgba(91, 95, 207, 0.3) !important;
|
||||
outline-offset: 2px !important;
|
||||
}
|
||||
|
||||
.editable-cell { vertical-align: middle; }
|
||||
.cell-click { cursor: pointer; display: block; min-height: 1.5em; }
|
||||
.cell-click:hover { background: var(--bg-hover, #f0f1ff); border-radius: 4px; }
|
||||
.cell-value { max-width: 280px; display: inline-block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.value-cell { max-width: 300px; }
|
||||
.inline-input, .inline-select, .inline-number {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border: 1px solid var(--border-primary, #e2e8f0);
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
background: var(--bg-primary, #fff);
|
||||
}
|
||||
.inline-select { min-width: 90px; cursor: pointer; }
|
||||
.inline-number { width: 70px; max-width: 80px; }
|
||||
.sortable-th { cursor: pointer; user-select: none; white-space: nowrap; }
|
||||
.sortable-th:hover { background: var(--bg-hover, #f0f1ff); }
|
||||
.sort-icon { margin-left: 4px; opacity: 0.7; font-size: 0.75rem; }
|
||||
.dns-search-wrap { position: relative; max-width: 440px; display: flex; align-items: center; }
|
||||
.dns-search-icon-left { display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 38px; margin-right: 0; padding-right: 0; color: #94a3b8; background: var(--bg-hover, #f1f5f9); border: 1px solid var(--border-primary, #e2e8f0); border-right: none; border-radius: 8px 0 0 8px; flex-shrink: 0; }
|
||||
.dns-search-input { flex: 1; padding-left: 12px; padding-right: 36px; border-radius: 0 8px 8px 0; border: 1px solid var(--border-primary, #e2e8f0); border-left: none; min-width: 0; }
|
||||
.dns-search-wrap:focus-within .dns-search-icon-left { background: var(--bg-primary, #fff); color: #5b5fcf; }
|
||||
.dns-search-clear { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); cursor: pointer; color: #94a3b8; padding: 4px; }
|
||||
.dns-search-clear:hover { color: #64748b; }
|
||||
|
||||
.dns-table-wrap { width: 100%; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.modern-container { padding: 1rem; }
|
||||
.page-header { margin-bottom: 1.5rem; }
|
||||
.page-title { font-size: 1.5rem; flex-wrap: wrap; justify-content: center; gap: 0.5rem; }
|
||||
.page-title .docs-link { font-size: 0.8rem; padding: 0.4rem 0.75rem; }
|
||||
.page-subtitle { font-size: 0.95rem; }
|
||||
.card-header { padding: 1rem 1.25rem; }
|
||||
.card-title { font-size: 1.1rem; }
|
||||
.card-body { padding: 1.25rem; }
|
||||
.modern-tabs { flex-wrap: wrap; gap: 0.5rem; margin-bottom: 1.25rem; }
|
||||
.modern-tab { padding: 0.75rem 1rem; font-size: 0.9rem; min-height: 44px; }
|
||||
.form-section { margin-bottom: 1.5rem; }
|
||||
.form-section .row.mb-4 .col-md-8 { margin-bottom: 0.75rem; }
|
||||
.form-section .row.mb-4 .col-md-4 { margin-top: 0 !important; }
|
||||
.form-section .row.mb-4 .col-md-4 .btn-primary { margin-top: 0; min-height: 44px; }
|
||||
.record-tabs { overflow-x: auto; overflow-y: hidden; -webkit-overflow-scrolling: touch; flex-wrap: nowrap; padding: 0.5rem; margin-bottom: 1rem; scrollbar-width: thin; }
|
||||
.record-tabs::-webkit-scrollbar { height: 6px; }
|
||||
.record-tab { flex: 0 0 auto; min-width: 44px; min-height: 44px; padding: 0.5rem 0.75rem; font-size: 0.8rem; }
|
||||
.record-form.active { flex-direction: column; align-items: stretch; }
|
||||
.record-form .form-group { min-width: 0; margin-bottom: 1rem; }
|
||||
.record-form .btn-primary { width: 100%; min-height: 44px; justify-content: center; }
|
||||
.dns-search-wrap { max-width: 100%; }
|
||||
.dns-table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; margin: 0 -1.25rem; padding: 0 1.25rem; touch-action: pan-x; }
|
||||
.records-table { margin-top: 1rem; font-size: 12px; }
|
||||
.records-table th, .records-table td { padding: 10px 8px; }
|
||||
.records-table th { font-size: 10px; }
|
||||
.records-table td:nth-child(1) { min-width: 140px; max-width: 200px; }
|
||||
.records-table td:nth-child(4) { min-width: 120px; max-width: 220px; }
|
||||
.records-table td:nth-child(6) { min-width: 56px; }
|
||||
.records-table td:nth-child(7) { min-width: 48px; min-height: 44px; }
|
||||
.delete-icon { min-width: 32px; min-height: 32px; padding: 6px; box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; }
|
||||
.inline-select { min-width: 70px; }
|
||||
.inline-number { width: 56px; max-width: 64px; }
|
||||
.sortable-th { padding: 10px 8px; min-height: 44px; }
|
||||
.btn-primary { min-height: 44px; padding: 0.75rem 1.25rem; }
|
||||
.edit-record-modal { margin: 1rem; padding: 1.25rem; width: 95%; max-width: none; }
|
||||
.disabled-notice { padding: 1.5rem; }
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.modern-container { padding: 0.75rem; }
|
||||
.page-title { font-size: 1.25rem; }
|
||||
.card-body { padding: 1rem; }
|
||||
.dns-table-wrap { margin: 0 -1rem; padding: 0 1rem; }
|
||||
.records-table th, .records-table td { padding: 8px 6px; font-size: 11px; }
|
||||
.records-table th { font-size: 9px; }
|
||||
.cell-value { max-width: 120px; }
|
||||
.value-cell { max-width: 140px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="modern-container" ng-controller="addModifyDNSRecordsCloudFlare">
|
||||
@@ -860,6 +959,12 @@
|
||||
{% trans "DNS Records" %}
|
||||
</h4>
|
||||
|
||||
<div class="dns-search-wrap mb-3" ng-if="!loadingRecords && records.length > 0">
|
||||
<span class="dns-search-icon-left"><i class="fas fa-search"></i></span>
|
||||
<input type="text" class="form-control dns-search-input" ng-model="dnsSearch.filter" placeholder="{% trans 'Search name, type, value...' %}" title="{% trans 'Search through all records' %}">
|
||||
<span class="dns-search-clear" ng-if="dnsSearch.filter" ng-click="dnsSearch.filter = ''" title="{% trans 'Clear search' %}"><i class="fas fa-times"></i></span>
|
||||
</div>
|
||||
|
||||
<div ng-if="loadingRecords" style="text-align: center; padding: 20px; color: #8893a7;">
|
||||
Loading DNS records...
|
||||
</div>
|
||||
@@ -868,29 +973,63 @@
|
||||
No DNS records found.
|
||||
</div>
|
||||
|
||||
<div ng-if="!loadingRecords && records.length > 0 && dnsSearch.filter && filteredRecords.length === 0" class="alert alert-info" style="margin-bottom: 1rem;">
|
||||
<i class="fas fa-info-circle"></i> {% trans "No records match your search." %}
|
||||
</div>
|
||||
|
||||
<div class="dns-table-wrap">
|
||||
<table class="records-table activity-table" ng-if="!loadingRecords && records.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Type" %}</th>
|
||||
<th>{% trans "TTL" %}</th>
|
||||
<th>{% trans "Value" %}</th>
|
||||
<th>{% trans "Priority" %}</th>
|
||||
<th>{% trans "Proxy" %}</th>
|
||||
<th class="sortable-th" ng-click="setSort('name')" title="{% trans 'Click to sort' %}">
|
||||
{% trans "Name" %} <i class="fas sort-icon" ng-class="{'fa-sort-up': sortColumn === 'name' && !sortReverse, 'fa-sort-down': sortColumn === 'name' && sortReverse, 'fa-sort': sortColumn !== 'name'}"></i>
|
||||
</th>
|
||||
<th class="sortable-th" ng-click="setSort('type')" title="{% trans 'Click to sort' %}">
|
||||
{% trans "Type" %} <i class="fas sort-icon" ng-class="{'fa-sort-up': sortColumn === 'type' && !sortReverse, 'fa-sort-down': sortColumn === 'type' && sortReverse, 'fa-sort': sortColumn !== 'type'}"></i>
|
||||
</th>
|
||||
<th class="sortable-th" ng-click="setSort('ttlNum')" title="{% trans 'Click to sort' %}">
|
||||
{% trans "TTL" %} <i class="fas sort-icon" ng-class="{'fa-sort-up': sortColumn === 'ttlNum' && !sortReverse, 'fa-sort-down': sortColumn === 'ttlNum' && sortReverse, 'fa-sort': sortColumn !== 'ttlNum'}"></i>
|
||||
</th>
|
||||
<th class="sortable-th" ng-click="setSort('content')" title="{% trans 'Click to sort' %}">
|
||||
{% trans "Value" %} <i class="fas sort-icon" ng-class="{'fa-sort-up': sortColumn === 'content' && !sortReverse, 'fa-sort-down': sortColumn === 'content' && sortReverse, 'fa-sort': sortColumn !== 'content'}"></i>
|
||||
</th>
|
||||
<th class="sortable-th" ng-click="setSort('priority')" title="{% trans 'Click to sort' %}">
|
||||
{% trans "Priority" %} <i class="fas sort-icon" ng-class="{'fa-sort-up': sortColumn === 'priority' && !sortReverse, 'fa-sort-down': sortColumn === 'priority' && sortReverse, 'fa-sort': sortColumn !== 'priority'}"></i>
|
||||
</th>
|
||||
<th class="sortable-th" ng-click="setSort('proxy')" title="{% trans 'Click to sort' %}">
|
||||
{% trans "Proxy" %} <i class="fas sort-icon" ng-class="{'fa-sort-up': sortColumn === 'proxy' && !sortReverse, 'fa-sort-down': sortColumn === 'proxy' && sortReverse, 'fa-sort': sortColumn !== 'proxy'}"></i>
|
||||
</th>
|
||||
<th style="text-align: center;">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="record in records track by $index">
|
||||
<td><strong ng-bind="record.name"></strong></td>
|
||||
<td>
|
||||
<span style="padding: 0.25rem 0.75rem; background: var(--bg-hover, #f0f1ff); color: #5b5fcf; border-radius: 4px; font-weight: 500; font-size: 0.75rem;">
|
||||
{{ record.type }}
|
||||
</span>
|
||||
<tr ng-repeat="record in filteredRecords track by record.id">
|
||||
<td class="editable-cell">
|
||||
<span ng-hide="isEditing(record, 'name')" ng-click="startEdit(record, 'name')" class="cell-click" title="{% trans 'Click to edit' %}"><strong ng-bind="record.name"></strong></span>
|
||||
<input ng-show="isEditing(record, 'name')" type="text" class="inline-input" ng-model="record.name" ng-blur="saveInlineField(record, 'name')" ng-keypress="$event.keyCode === 13 && saveInlineField(record, 'name')">
|
||||
</td>
|
||||
<td class="editable-cell">
|
||||
<select class="inline-select" ng-model="record.type" ng-options="t as t for t in getTypeOptions(record)" ng-change="saveInlineField(record, 'type')" title="{% trans 'Type' %}">
|
||||
</select>
|
||||
</td>
|
||||
<td class="editable-cell">
|
||||
<select class="inline-select" ng-model="record.ttlNum" ng-change="saveInlineField(record, 'ttl')" title="{% trans 'TTL' %}">
|
||||
<option ng-value="1">AUTO</option>
|
||||
<option ng-value="300">300</option>
|
||||
<option ng-value="600">600</option>
|
||||
<option ng-value="3600">3600</option>
|
||||
<option ng-value="7200">7200</option>
|
||||
<option ng-value="14400">14400</option>
|
||||
<option ng-value="86400">86400</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="editable-cell value-cell">
|
||||
<span ng-hide="isEditing(record, 'content')" ng-click="startEdit(record, 'content')" class="cell-click" title="{% trans 'Click to edit' %}"><span class="cell-value" ng-bind="record.content" title="{{ record.content }}"></span></span>
|
||||
<input ng-show="isEditing(record, 'content')" type="text" class="inline-input" ng-model="record.content" ng-blur="saveInlineField(record, 'content')" ng-keypress="$event.keyCode === 13 && saveInlineField(record, 'content')">
|
||||
</td>
|
||||
<td class="editable-cell">
|
||||
<input type="number" class="inline-number" ng-model="record.priority" ng-blur="saveInlineField(record, 'priority')" ng-keypress="$event.keyCode === 13 && saveInlineField(record, 'priority')" min="0" step="1" title="{% trans 'Priority' %}">
|
||||
</td>
|
||||
<td ng-bind="record.ttl"></td>
|
||||
<td style="max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" ng-bind="record.content" title="{{ record.content }}"></td>
|
||||
<td ng-bind="record.priority || '-'"></td>
|
||||
<td>
|
||||
<input class="proxy-toggle"
|
||||
ng-click="enableProxy(record.name, record.proxy)"
|
||||
@@ -933,10 +1072,61 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Edit DNS Record Modal -->
|
||||
<div class="edit-record-overlay" ng-show="showEditModal" ng-click="closeEditModal()">
|
||||
<div class="edit-record-modal" ng-click="$event.stopPropagation()">
|
||||
<h4 class="mb-4" style="color: var(--text-primary, #1e293b); font-weight: 600;">
|
||||
<i class="fas fa-edit"></i> {% trans "Edit DNS Record" %}
|
||||
</h4>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{% trans "Name" %}</label>
|
||||
<input type="text" class="form-control" ng-model="editRecord.name" placeholder="e.g. www or @">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{% trans "Type" %}</label>
|
||||
<select class="form-control" ng-model="editRecord.type">
|
||||
<option value="A">A</option>
|
||||
<option value="AAAA">AAAA</option>
|
||||
<option value="CNAME">CNAME</option>
|
||||
<option value="MX">MX</option>
|
||||
<option value="TXT">TXT</option>
|
||||
<option value="NS">NS</option>
|
||||
<option value="SOA">SOA</option>
|
||||
<option value="SRV">SRV</option>
|
||||
<option value="CAA">CAA</option>
|
||||
<option value="SPF">SPF</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{% trans "TTL" %}</label>
|
||||
<input type="text" class="form-control" ng-model="editRecord.ttl" placeholder="3600 or AUTO">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{% trans "Value / Content" %}</label>
|
||||
<input type="text" class="form-control" ng-model="editRecord.content" placeholder="Record value">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{% trans "Priority" %}</label>
|
||||
<input type="number" class="form-control" ng-model="editRecord.priority" placeholder="0">
|
||||
</div>
|
||||
<div class="form-group" ng-if="editRecord.proxiable">
|
||||
<label class="form-label">{% trans "Proxy (orange cloud)" %}</label>
|
||||
<input type="checkbox" ng-model="editRecord.proxy" style="margin-left: 8px;">
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 1.5rem;">
|
||||
<button type="button" class="btn-secondary" ng-click="closeEditModal()">{% trans "Cancel" %}</button>
|
||||
<button type="button" class="btn-primary" ng-click="saveEditRecord()">
|
||||
<i class="fas fa-save"></i> {% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alert Messages -->
|
||||
<div style="margin-top: 2rem;">
|
||||
|
||||
@@ -27,6 +27,7 @@ urlpatterns = [
|
||||
re_path(r'^getCurrentRecordsForDomainCloudFlare$', views.getCurrentRecordsForDomainCloudFlare, name='getCurrentRecordsForDomainCloudFlare'),
|
||||
re_path(r'^deleteDNSRecordCloudFlare$', views.deleteDNSRecordCloudFlare, name='deleteDNSRecordCloudFlare'),
|
||||
re_path(r'^addDNSRecordCloudFlare$', views.addDNSRecordCloudFlare, name='addDNSRecordCloudFlare'),
|
||||
re_path(r'^updateDNSRecordCloudFlare$', views.updateDNSRecordCloudFlare, name='updateDNSRecordCloudFlare'),
|
||||
re_path(r'^syncCF$', views.syncCF, name='syncCF'),
|
||||
re_path(r'^enableProxy$', views.enableProxy, name='enableProxy'),
|
||||
]
|
||||
|
||||
23
dns/views.py
23
dns/views.py
@@ -199,7 +199,12 @@ def addDeleteDNSRecordsCloudFlare(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
dm = DNSManager()
|
||||
return dm.addDeleteDNSRecordsCloudFlare(request, userID)
|
||||
response = dm.addDeleteDNSRecordsCloudFlare(request, userID)
|
||||
if hasattr(response, 'headers'):
|
||||
response['Cache-Control'] = 'no-cache, no-store, must-revalidate, max-age=0'
|
||||
response['Pragma'] = 'no-cache'
|
||||
response['Expires'] = '0'
|
||||
return response
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
@@ -339,6 +344,15 @@ def addDNSRecordCloudFlare(request):
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
|
||||
def updateDNSRecordCloudFlare(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
dm = DNSManager()
|
||||
return dm.updateDNSRecordCloudFlare(userID, json.loads(request.body))
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
|
||||
def syncCF(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
@@ -355,10 +369,11 @@ def syncCF(request):
|
||||
def enableProxy(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
|
||||
body = json.loads(request.body or '{}')
|
||||
dm = DNSManager()
|
||||
coreResult = dm.enableProxy(userID, json.loads(request.body))
|
||||
|
||||
coreResult = dm.enableProxy(userID, body)
|
||||
return coreResult
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
except (ValueError, TypeError):
|
||||
return HttpResponse(json.dumps({'status': 0, 'error_message': 'Invalid request'}), status=400, content_type='application/json')
|
||||
|
||||
BIN
emailMarketing/.DS_Store
vendored
BIN
emailMarketing/.DS_Store
vendored
Binary file not shown.
@@ -1 +0,0 @@
|
||||
default_app_config = 'emailMarketing.apps.EmailmarketingConfig'
|
||||
@@ -1,6 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
@@ -1,10 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class EmailmarketingConfig(AppConfig):
|
||||
name = 'emailMarketing'
|
||||
def ready(self):
|
||||
from . import signals
|
||||
@@ -1,66 +0,0 @@
|
||||
from .models import EmailMarketing, EmailTemplate, SMTPHosts, EmailLists, EmailJobs
|
||||
from websiteFunctions.models import Websites
|
||||
|
||||
class emACL:
|
||||
|
||||
@staticmethod
|
||||
def checkIfEMEnabled(userName):
|
||||
try:
|
||||
user = EmailMarketing.objects.get(userName=userName)
|
||||
return 0
|
||||
except:
|
||||
return 1
|
||||
|
||||
@staticmethod
|
||||
def getEmailsLists(domain):
|
||||
website = Websites.objects.get(domain=domain)
|
||||
emailLists = website.emaillists_set.all()
|
||||
listNames = []
|
||||
|
||||
for items in emailLists:
|
||||
listNames.append(items.listName)
|
||||
|
||||
return listNames
|
||||
|
||||
@staticmethod
|
||||
def allTemplates(currentACL, admin):
|
||||
if currentACL['admin'] == 1:
|
||||
allTemplates = EmailTemplate.objects.all()
|
||||
else:
|
||||
allTemplates = admin.emailtemplate_set.all()
|
||||
|
||||
templateNames = []
|
||||
for items in allTemplates:
|
||||
templateNames.append(items.name)
|
||||
return templateNames
|
||||
|
||||
@staticmethod
|
||||
def allSMTPHosts(currentACL, admin):
|
||||
if currentACL['admin'] == 1:
|
||||
allHosts = SMTPHosts.objects.all()
|
||||
else:
|
||||
allHosts = admin.smtphosts_set.all()
|
||||
hostNames = []
|
||||
|
||||
for items in allHosts:
|
||||
hostNames.append(items.host)
|
||||
|
||||
return hostNames
|
||||
|
||||
@staticmethod
|
||||
def allEmailsLists(currentACL, admin):
|
||||
listNames = []
|
||||
emailLists = EmailLists.objects.all()
|
||||
|
||||
if currentACL['admin'] == 1:
|
||||
for items in emailLists:
|
||||
listNames.append(items.listName)
|
||||
else:
|
||||
for items in emailLists:
|
||||
if items.owner.admin == admin:
|
||||
listNames.append(items.listName)
|
||||
|
||||
return listNames
|
||||
|
||||
|
||||
|
||||
@@ -1,427 +0,0 @@
|
||||
#!/usr/local/CyberCP/bin/python
|
||||
|
||||
import os
|
||||
import time
|
||||
import csv
|
||||
import re
|
||||
import plogical.CyberCPLogFileWriter as logging
|
||||
from .models import EmailLists, EmailsInList, EmailTemplate, EmailJobs, SMTPHosts, ValidationLog
|
||||
from plogical.backupSchedule import backupSchedule
|
||||
from websiteFunctions.models import Websites
|
||||
import threading as multi
|
||||
import socket, smtplib
|
||||
import DNS
|
||||
from random import randint
|
||||
from plogical.processUtilities import ProcessUtilities
|
||||
|
||||
class emailMarketing(multi.Thread):
|
||||
def __init__(self, function, extraArgs):
|
||||
multi.Thread.__init__(self)
|
||||
self.function = function
|
||||
self.extraArgs = extraArgs
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
if self.function == 'createEmailList':
|
||||
self.createEmailList()
|
||||
elif self.function == 'verificationJob':
|
||||
self.verificationJob()
|
||||
elif self.function == 'startEmailJob':
|
||||
self.startEmailJob()
|
||||
except BaseException as msg:
|
||||
logging.CyberCPLogFileWriter.writeToFile(str(msg) + ' [emailMarketing.run]')
|
||||
|
||||
def createEmailList(self):
|
||||
try:
|
||||
website = Websites.objects.get(domain=self.extraArgs['domain'])
|
||||
try:
|
||||
newList = EmailLists(owner=website, listName=self.extraArgs['listName'], dateCreated=time.strftime("%I-%M-%S-%a-%b-%Y"))
|
||||
newList.save()
|
||||
except:
|
||||
newList = EmailLists.objects.get(listName=self.extraArgs['listName'])
|
||||
|
||||
counter = 0
|
||||
|
||||
if self.extraArgs['path'].endswith('.csv'):
|
||||
with open(self.extraArgs['path'], 'r') as emailsList:
|
||||
data = csv.reader(emailsList, delimiter=',')
|
||||
for items in data:
|
||||
try:
|
||||
for value in items:
|
||||
if re.match('^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$', value) != None:
|
||||
try:
|
||||
getEmail = EmailsInList.objects.get(owner=newList, email=value)
|
||||
except:
|
||||
try:
|
||||
newEmail = EmailsInList(owner=newList, email=value,
|
||||
verificationStatus='NOT CHECKED',
|
||||
dateCreated=time.strftime("%I-%M-%S-%a-%b-%Y"))
|
||||
newEmail.save()
|
||||
except:
|
||||
pass
|
||||
logging.CyberCPLogFileWriter.statusWriter(self.extraArgs['tempStatusPath'], str(counter) + ' emails read.')
|
||||
counter = counter + 1
|
||||
except BaseException as msg:
|
||||
logging.CyberCPLogFileWriter.writeToFile('%s. [createEmailList]' % (str(msg)))
|
||||
continue
|
||||
elif self.extraArgs['path'].endswith('.txt'):
|
||||
with open(self.extraArgs['path'], 'r') as emailsList:
|
||||
emails = emailsList.readline()
|
||||
while emails:
|
||||
email = emails.strip('\n')
|
||||
if re.match('^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$', email) != None:
|
||||
try:
|
||||
getEmail = EmailsInList.objects.get(owner=newList, email=email)
|
||||
except BaseException as msg:
|
||||
newEmail = EmailsInList(owner=newList, email=email, verificationStatus='NOT CHECKED',
|
||||
dateCreated=time.strftime("%I-%M-%S-%a-%b-%Y"))
|
||||
newEmail.save()
|
||||
logging.CyberCPLogFileWriter.statusWriter(self.extraArgs['tempStatusPath'],str(counter) + ' emails read.')
|
||||
counter = counter + 1
|
||||
emails = emailsList.readline()
|
||||
|
||||
logging.CyberCPLogFileWriter.statusWriter(self.extraArgs['tempStatusPath'], str(counter) + 'Successfully read all emails. [200]')
|
||||
except BaseException as msg:
|
||||
logging.CyberCPLogFileWriter.statusWriter(self.extraArgs['tempStatusPath'], str(msg) +'. [404]')
|
||||
return 0
|
||||
|
||||
def findNextIP(self):
|
||||
try:
|
||||
if self.delayData['rotation'] == 'Disable':
|
||||
return None
|
||||
elif self.delayData['rotation'] == 'IPv4':
|
||||
if self.delayData['ipv4'].find(',') == -1:
|
||||
return self.delayData['ipv4']
|
||||
else:
|
||||
ipv4s = self.delayData['ipv4'].split(',')
|
||||
|
||||
if self.currentIP == '':
|
||||
return ipv4s[0]
|
||||
else:
|
||||
returnCheck = 0
|
||||
|
||||
for items in ipv4s:
|
||||
if returnCheck == 1:
|
||||
return items
|
||||
if items == self.currentIP:
|
||||
returnCheck = 1
|
||||
|
||||
return ipv4s[0]
|
||||
else:
|
||||
if self.delayData['ipv6'].find(',') == -1:
|
||||
return self.delayData['ipv6']
|
||||
else:
|
||||
ipv6 = self.delayData['ipv6'].split(',')
|
||||
|
||||
if self.currentIP == '':
|
||||
return ipv6[0]
|
||||
else:
|
||||
returnCheck = 0
|
||||
|
||||
for items in ipv6:
|
||||
if returnCheck == 1:
|
||||
return items
|
||||
if items == self.currentIP:
|
||||
returnCheck = 1
|
||||
return ipv6[0]
|
||||
except BaseException as msg:
|
||||
logging.CyberCPLogFileWriter.writeToFile(str(msg))
|
||||
return None
|
||||
|
||||
def verificationJob(self):
|
||||
try:
|
||||
|
||||
verificationList = EmailLists.objects.get(listName=self.extraArgs['listName'])
|
||||
domain = verificationList.owner.domain
|
||||
|
||||
if not os.path.exists('/home/cyberpanel/' + domain):
|
||||
os.mkdir('/home/cyberpanel/' + domain)
|
||||
|
||||
tempStatusPath = '/home/cyberpanel/' + domain + "/" + self.extraArgs['listName']
|
||||
logging.CyberCPLogFileWriter.statusWriter(tempStatusPath, 'Starting verification job..')
|
||||
|
||||
counter = 1
|
||||
counterGlobal = 0
|
||||
|
||||
allEmailsInList = verificationList.emailsinlist_set.all()
|
||||
|
||||
configureVerifyPath = '/home/cyberpanel/configureVerify'
|
||||
finalPath = '%s/%s' % (configureVerifyPath, domain)
|
||||
|
||||
|
||||
import json
|
||||
if os.path.exists(finalPath):
|
||||
self.delayData = json.loads(open(finalPath, 'r').read())
|
||||
|
||||
self.currentIP = ''
|
||||
|
||||
ValidationLog(owner=verificationList, status=backupSchedule.INFO, message='Starting email verification..').save()
|
||||
|
||||
for items in allEmailsInList:
|
||||
if items.verificationStatus != 'Verified':
|
||||
try:
|
||||
|
||||
email = items.email
|
||||
self.currentEmail = email
|
||||
domainName = email.split('@')[1]
|
||||
records = DNS.dnslookup(domainName, 'MX', 15)
|
||||
|
||||
counterGlobal = counterGlobal + 1
|
||||
|
||||
for mxRecord in records:
|
||||
|
||||
# Get local server hostname
|
||||
host = socket.gethostname()
|
||||
|
||||
## Only fetching smtp object
|
||||
|
||||
if os.path.exists(finalPath):
|
||||
try:
|
||||
delay = self.delayData['delay']
|
||||
if delay == 'Enable':
|
||||
if counterGlobal == int(self.delayData['delayAfter']):
|
||||
ValidationLog(owner=verificationList, status=backupSchedule.INFO,
|
||||
message='Sleeping for %s seconds...' % (self.delayData['delayTime'])).save()
|
||||
|
||||
time.sleep(int(self.delayData['delayTime']))
|
||||
counterGlobal = 0
|
||||
self.currentIP = self.findNextIP()
|
||||
|
||||
ValidationLog(owner=verificationList, status=backupSchedule.INFO,
|
||||
message='IP being used for validation until next sleep: %s.' % (str(self.currentIP))).save()
|
||||
|
||||
if self.currentIP == None:
|
||||
server = smtplib.SMTP(timeout=10)
|
||||
else:
|
||||
server = smtplib.SMTP(self.currentIP, timeout=10)
|
||||
else:
|
||||
|
||||
if self.currentIP == '':
|
||||
self.currentIP = self.findNextIP()
|
||||
ValidationLog(owner=verificationList, status=backupSchedule.INFO,
|
||||
message='IP being used for validation until next sleep: %s.' % (
|
||||
str(self.currentIP))).save()
|
||||
|
||||
if self.currentIP == None:
|
||||
server = smtplib.SMTP(timeout=10)
|
||||
else:
|
||||
server = smtplib.SMTP(self.currentIP, timeout=10)
|
||||
else:
|
||||
logging.CyberCPLogFileWriter.writeToFile(
|
||||
'Delay not configured..')
|
||||
|
||||
ValidationLog(owner=verificationList, status=backupSchedule.INFO,
|
||||
message='Delay not configured..').save()
|
||||
|
||||
server = smtplib.SMTP(timeout=10)
|
||||
except BaseException as msg:
|
||||
|
||||
ValidationLog(owner=verificationList, status=backupSchedule.ERROR,
|
||||
message='Delay not configured. Error message: %s' % (str(msg))).save()
|
||||
|
||||
server = smtplib.SMTP(timeout=10)
|
||||
else:
|
||||
server = smtplib.SMTP(timeout=10)
|
||||
|
||||
###
|
||||
|
||||
server.set_debuglevel(0)
|
||||
|
||||
# SMTP Conversation
|
||||
server.connect(mxRecord[1])
|
||||
server.helo(host)
|
||||
server.mail('host' + "@" + host)
|
||||
code, message = server.rcpt(str(email))
|
||||
server.quit()
|
||||
|
||||
# Assume 250 as Success
|
||||
if code == 250:
|
||||
items.verificationStatus = 'Verified'
|
||||
items.save()
|
||||
break
|
||||
else:
|
||||
ValidationLog(owner=verificationList, status=backupSchedule.ERROR,
|
||||
message='Failed to verify %s. Error message %s' % (email, message.decode())).save()
|
||||
items.verificationStatus = 'Verification Failed'
|
||||
items.save()
|
||||
|
||||
logging.CyberCPLogFileWriter.statusWriter(tempStatusPath, str(counter) + ' emails verified so far..')
|
||||
counter = counter + 1
|
||||
except BaseException as msg:
|
||||
items.verificationStatus = 'Verification Failed'
|
||||
items.save()
|
||||
counter = counter + 1
|
||||
ValidationLog(owner=verificationList, status=backupSchedule.ERROR,
|
||||
message='Failed to verify %s. Error message %s' % (
|
||||
self.currentEmail , str(msg))).save()
|
||||
|
||||
|
||||
verificationList.notVerified = verificationList.emailsinlist_set.filter(verificationStatus='Verification Failed').count()
|
||||
verificationList.verified = verificationList.emailsinlist_set.filter(verificationStatus='Verified').count()
|
||||
verificationList.save()
|
||||
|
||||
ValidationLog(owner=verificationList, status=backupSchedule.ERROR, message=str(counter) + ' emails successfully verified. [200]').save()
|
||||
|
||||
logging.CyberCPLogFileWriter.statusWriter(tempStatusPath, str(counter) + ' emails successfully verified. [200]')
|
||||
except BaseException as msg:
|
||||
verificationList = EmailLists.objects.get(listName=self.extraArgs['listName'])
|
||||
domain = verificationList.owner.domain
|
||||
tempStatusPath = '/home/cyberpanel/' + domain + "/" + self.extraArgs['listName']
|
||||
logging.CyberCPLogFileWriter.statusWriter(tempStatusPath, str(msg) +'. [404]')
|
||||
logging.CyberCPLogFileWriter.writeToFile(str(msg))
|
||||
return 0
|
||||
|
||||
def setupSMTPConnection(self):
|
||||
try:
|
||||
if self.extraArgs['host'] == 'localhost':
|
||||
self.smtpServer = smtplib.SMTP('127.0.0.1')
|
||||
return 1
|
||||
else:
|
||||
self.verifyHost = SMTPHosts.objects.get(host=self.extraArgs['host'])
|
||||
self.smtpServer = smtplib.SMTP(str(self.verifyHost.host), int(self.verifyHost.port))
|
||||
|
||||
if int(self.verifyHost.port) == 587:
|
||||
self.smtpServer.starttls()
|
||||
|
||||
self.smtpServer.login(str(self.verifyHost.userName), str(self.verifyHost.password))
|
||||
return 1
|
||||
except smtplib.SMTPHeloError:
|
||||
logging.CyberCPLogFileWriter.statusWriter(self.extraArgs['tempStatusPath'],
|
||||
'The server didnt reply properly to the HELO greeting.')
|
||||
return 0
|
||||
except smtplib.SMTPAuthenticationError:
|
||||
logging.CyberCPLogFileWriter.statusWriter(self.extraArgs['tempStatusPath'],
|
||||
'Username and password combination not accepted.')
|
||||
return 0
|
||||
except smtplib.SMTPException:
|
||||
logging.CyberCPLogFileWriter.statusWriter(self.extraArgs['tempStatusPath'],
|
||||
'No suitable authentication method was found.')
|
||||
return 0
|
||||
|
||||
def startEmailJob(self):
|
||||
try:
|
||||
|
||||
if self.setupSMTPConnection() == 0:
|
||||
logging.CyberCPLogFileWriter.writeToFile('SMTP Connection failed. [301]')
|
||||
return 0
|
||||
|
||||
emailList = EmailLists.objects.get(listName=self.extraArgs['listName'])
|
||||
allEmails = emailList.emailsinlist_set.all()
|
||||
emailMessage = EmailTemplate.objects.get(name=self.extraArgs['selectedTemplate'])
|
||||
|
||||
totalEmails = allEmails.count()
|
||||
sent = 0
|
||||
failed = 0
|
||||
|
||||
ipFile = "/etc/cyberpanel/machineIP"
|
||||
f = open(ipFile)
|
||||
ipData = f.read()
|
||||
ipAddress = ipData.split('\n', 1)[0]
|
||||
|
||||
## Compose Message
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
import re
|
||||
|
||||
tempPath = "/home/cyberpanel/" + str(randint(1000, 9999))
|
||||
|
||||
emailJob = EmailJobs(owner=emailMessage, date=time.strftime("%I-%M-%S-%a-%b-%Y"),
|
||||
host=self.extraArgs['host'], totalEmails=totalEmails,
|
||||
sent=sent, failed=failed
|
||||
)
|
||||
emailJob.save()
|
||||
|
||||
for items in allEmails:
|
||||
try:
|
||||
message = MIMEMultipart('alternative')
|
||||
message['Subject'] = emailMessage.subject
|
||||
message['From'] = emailMessage.fromEmail
|
||||
message['reply-to'] = emailMessage.replyTo
|
||||
|
||||
if (items.verificationStatus == 'Verified' or self.extraArgs[
|
||||
'verificationCheck']) and not items.verificationStatus == 'REMOVED':
|
||||
try:
|
||||
port = ProcessUtilities.fetchCurrentPort()
|
||||
removalLink = "https:\/\/" + ipAddress + ":%s\/emailMarketing\/remove\/" % (port) + self.extraArgs[
|
||||
'listName'] + "\/" + items.email
|
||||
messageText = emailMessage.emailMessage.encode('utf-8', 'replace')
|
||||
message['To'] = items.email
|
||||
|
||||
if re.search(b'<html', messageText, re.IGNORECASE) and re.search(b'<body', messageText,
|
||||
re.IGNORECASE):
|
||||
finalMessage = messageText.decode()
|
||||
|
||||
self.extraArgs['unsubscribeCheck'] = 0
|
||||
if self.extraArgs['unsubscribeCheck']:
|
||||
messageFile = open(tempPath, 'w')
|
||||
messageFile.write(finalMessage)
|
||||
messageFile.close()
|
||||
|
||||
command = "sudo sed -i 's/{{ unsubscribeCheck }}/" + removalLink + "/g' " + tempPath
|
||||
ProcessUtilities.executioner(command, 'cyberpanel')
|
||||
|
||||
messageFile = open(tempPath, 'r')
|
||||
finalMessage = messageFile.read()
|
||||
messageFile.close()
|
||||
|
||||
html = MIMEText(finalMessage, 'html')
|
||||
message.attach(html)
|
||||
|
||||
else:
|
||||
finalMessage = messageText
|
||||
|
||||
if self.extraArgs['unsubscribeCheck']:
|
||||
finalMessage = finalMessage.replace('{{ unsubscribeCheck }}', removalLink)
|
||||
|
||||
html = MIMEText(finalMessage, 'plain')
|
||||
message.attach(html)
|
||||
|
||||
try:
|
||||
status = self.smtpServer.noop()[0]
|
||||
self.smtpServer.sendmail(message['From'], items.email, message.as_string())
|
||||
except: # smtplib.SMTPServerDisconnected
|
||||
if self.setupSMTPConnection() == 0:
|
||||
logging.CyberCPLogFileWriter.writeToFile('SMTP Connection failed. [301]')
|
||||
return 0
|
||||
self.smtpServer.sendmail(message['From'], items.email, message.as_string())
|
||||
|
||||
|
||||
sent = sent + 1
|
||||
emailJob.sent = sent
|
||||
emailJob.save()
|
||||
logging.CyberCPLogFileWriter.statusWriter(self.extraArgs['tempStatusPath'],
|
||||
'Successfully sent: ' + str(
|
||||
sent) + ' Failed: ' + str(
|
||||
failed))
|
||||
except BaseException as msg:
|
||||
failed = failed + 1
|
||||
emailJob.failed = failed
|
||||
emailJob.save()
|
||||
logging.CyberCPLogFileWriter.statusWriter(self.extraArgs['tempStatusPath'],
|
||||
'Successfully sent: ' + str(
|
||||
sent) + ', Failed: ' + str(failed))
|
||||
if self.setupSMTPConnection() == 0:
|
||||
logging.CyberCPLogFileWriter.writeToFile(
|
||||
'SMTP Connection failed. Error: %s. [392]' % (str(msg)))
|
||||
return 0
|
||||
except BaseException as msg:
|
||||
failed = failed + 1
|
||||
emailJob.failed = failed
|
||||
emailJob.save()
|
||||
logging.CyberCPLogFileWriter.statusWriter(self.extraArgs['tempStatusPath'],
|
||||
'Successfully sent: ' + str(
|
||||
sent) + ', Failed: ' + str(failed))
|
||||
if self.setupSMTPConnection() == 0:
|
||||
logging.CyberCPLogFileWriter.writeToFile('SMTP Connection failed. Error: %s. [399]' % (str(msg)))
|
||||
return 0
|
||||
|
||||
|
||||
emailJob.sent = sent
|
||||
emailJob.failed = failed
|
||||
emailJob.save()
|
||||
|
||||
logging.CyberCPLogFileWriter.statusWriter(self.extraArgs['tempStatusPath'],
|
||||
'Email job completed. [200]')
|
||||
except BaseException as msg:
|
||||
logging.CyberCPLogFileWriter.statusWriter(self.extraArgs['tempStatusPath'], str(msg) + '. [404]')
|
||||
return 0
|
||||
@@ -1,899 +0,0 @@
|
||||
from django.shortcuts import render, HttpResponse, redirect
|
||||
from plogical.acl import ACLManager
|
||||
from loginSystem.views import loadLoginPage
|
||||
import json
|
||||
from random import randint
|
||||
import time
|
||||
from plogical.httpProc import httpProc
|
||||
from .models import EmailMarketing, EmailLists, EmailsInList, EmailJobs
|
||||
from websiteFunctions.models import Websites
|
||||
from .emailMarketing import emailMarketing as EM
|
||||
from math import ceil
|
||||
import smtplib
|
||||
from .models import SMTPHosts, EmailTemplate
|
||||
from loginSystem.models import Administrator
|
||||
from .emACL import emACL
|
||||
|
||||
class EmailMarketingManager:
|
||||
|
||||
def __init__(self, request = None, domain = None):
|
||||
self.request = request
|
||||
self.domain = domain
|
||||
|
||||
def emailMarketing(self):
|
||||
proc = httpProc(self.request, 'emailMarketing/emailMarketing.html', None, 'admin')
|
||||
return proc.render()
|
||||
|
||||
def fetchUsers(self):
|
||||
try:
|
||||
|
||||
userID = self.request.session['userID']
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
|
||||
if currentACL['admin'] == 1:
|
||||
pass
|
||||
else:
|
||||
return ACLManager.loadError()
|
||||
|
||||
allUsers = ACLManager.findAllUsers()
|
||||
disabledUsers = EmailMarketing.objects.all()
|
||||
disabled = []
|
||||
for items in disabledUsers:
|
||||
disabled.append(items.userName)
|
||||
|
||||
json_data = "["
|
||||
checker = 0
|
||||
counter = 1
|
||||
|
||||
for items in allUsers:
|
||||
if items in disabled:
|
||||
status = 0
|
||||
else:
|
||||
status = 1
|
||||
|
||||
dic = {'id': counter, 'userName': items, 'status': status}
|
||||
|
||||
if checker == 0:
|
||||
json_data = json_data + json.dumps(dic)
|
||||
checker = 1
|
||||
else:
|
||||
json_data = json_data + ',' + json.dumps(dic)
|
||||
|
||||
counter = counter + 1
|
||||
|
||||
json_data = json_data + ']'
|
||||
data_ret = {"status": 1, 'data': json_data}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
except BaseException as msg:
|
||||
final_dic = {'status': 0, 'error_message': str(msg)}
|
||||
final_json = json.dumps(final_dic)
|
||||
return HttpResponse(final_json)
|
||||
|
||||
def enableDisableMarketing(self):
|
||||
try:
|
||||
|
||||
userID = self.request.session['userID']
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
|
||||
if currentACL['admin'] == 1:
|
||||
pass
|
||||
else:
|
||||
return ACLManager.loadErrorJson()
|
||||
|
||||
data = json.loads(self.request.body)
|
||||
userName = data['userName']
|
||||
|
||||
try:
|
||||
disableMarketing = EmailMarketing.objects.get(userName=userName)
|
||||
disableMarketing.delete()
|
||||
except:
|
||||
enableMarketing = EmailMarketing(userName=userName)
|
||||
enableMarketing.save()
|
||||
|
||||
data_ret = {"status": 1}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
except BaseException as msg:
|
||||
final_dic = {'status': 0, 'error_message': str(msg)}
|
||||
final_json = json.dumps(final_dic)
|
||||
return HttpResponse(final_json)
|
||||
|
||||
def createEmailList(self):
|
||||
try:
|
||||
userID = self.request.session['userID']
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
|
||||
if ACLManager.checkOwnership(self.domain, admin, currentACL) == 1:
|
||||
pass
|
||||
else:
|
||||
return ACLManager.loadError()
|
||||
|
||||
if emACL.checkIfEMEnabled(admin.userName) == 0:
|
||||
return ACLManager.loadError()
|
||||
|
||||
proc = httpProc(self.request, 'emailMarketing/createEmailList.html', {'domain': self.domain})
|
||||
return proc.render()
|
||||
except KeyError as msg:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
def submitEmailList(self):
|
||||
try:
|
||||
|
||||
data = json.loads(self.request.body)
|
||||
|
||||
extraArgs = {}
|
||||
extraArgs['domain'] = data['domain']
|
||||
extraArgs['path'] = data['path']
|
||||
extraArgs['listName'] = data['listName'].replace(' ', '')
|
||||
extraArgs['tempStatusPath'] = "/home/cyberpanel/" + str(randint(1000, 9999))
|
||||
|
||||
userID = self.request.session['userID']
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
|
||||
if ACLManager.checkOwnership(data['domain'], admin, currentACL) == 1:
|
||||
pass
|
||||
else:
|
||||
return ACLManager.loadErrorJson()
|
||||
|
||||
if emACL.checkIfEMEnabled(admin.userName) == 0:
|
||||
return ACLManager.loadErrorJson()
|
||||
|
||||
# em = EM('createEmailList', extraArgs)
|
||||
# em.start()
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
data_ret = {"status": 1, 'tempStatusPath': extraArgs['tempStatusPath']}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
except BaseException as msg:
|
||||
final_dic = {'status': 0, 'error_message': str(msg)}
|
||||
final_json = json.dumps(final_dic)
|
||||
return HttpResponse(final_json)
|
||||
|
||||
def manageLists(self):
|
||||
try:
|
||||
userID = self.request.session['userID']
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
|
||||
if ACLManager.checkOwnership(self.domain, admin, currentACL) == 1:
|
||||
pass
|
||||
else:
|
||||
return ACLManager.loadError()
|
||||
|
||||
if emACL.checkIfEMEnabled(admin.userName) == 0:
|
||||
return ACLManager.loadError()
|
||||
|
||||
listNames = emACL.getEmailsLists(self.domain)
|
||||
|
||||
proc = httpProc(self.request, 'emailMarketing/manageLists.html', {'listNames': listNames, 'domain': self.domain})
|
||||
return proc.render()
|
||||
|
||||
except KeyError as msg:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
def configureVerify(self):
|
||||
try:
|
||||
|
||||
userID = self.request.session['userID']
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
|
||||
if ACLManager.checkOwnership(self.domain, admin, currentACL) == 1:
|
||||
pass
|
||||
else:
|
||||
return ACLManager.loadError()
|
||||
|
||||
if emACL.checkIfEMEnabled(admin.userName) == 0:
|
||||
return ACLManager.loadError()
|
||||
|
||||
proc = httpProc(self.request, 'emailMarketing/configureVerify.html',
|
||||
{'domain': self.domain})
|
||||
return proc.render()
|
||||
|
||||
except KeyError as msg:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
def fetchVerifyLogs(self):
|
||||
try:
|
||||
|
||||
userID = self.request.session['userID']
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
|
||||
data = json.loads(self.request.body)
|
||||
|
||||
self.listName = data['listName']
|
||||
recordsToShow = int(data['recordsToShow'])
|
||||
page = int(str(data['page']).strip('\n'))
|
||||
|
||||
emailList = EmailLists.objects.get(listName=self.listName)
|
||||
|
||||
if ACLManager.checkOwnership(emailList.owner.domain, admin, currentACL) == 1:
|
||||
pass
|
||||
else:
|
||||
return ACLManager.loadErrorJson('status', 0)
|
||||
|
||||
logsLen = emailList.validationlog_set.all().count()
|
||||
|
||||
from s3Backups.s3Backups import S3Backups
|
||||
|
||||
pagination = S3Backups.getPagination(logsLen, recordsToShow)
|
||||
endPageNumber, finalPageNumber = S3Backups.recordsPointer(page, recordsToShow)
|
||||
finalLogs = emailList.validationlog_set.all()[finalPageNumber:endPageNumber]
|
||||
|
||||
json_data = "["
|
||||
checker = 0
|
||||
counter = 0
|
||||
|
||||
from plogical.backupSchedule import backupSchedule
|
||||
|
||||
for log in emailList.validationlog_set.all()[finalPageNumber:endPageNumber]:
|
||||
if log.status == backupSchedule.INFO:
|
||||
status = 'INFO'
|
||||
else:
|
||||
status = 'ERROR'
|
||||
|
||||
dic = {
|
||||
'status': status, "message": log.message
|
||||
}
|
||||
|
||||
if checker == 0:
|
||||
json_data = json_data + json.dumps(dic)
|
||||
checker = 1
|
||||
else:
|
||||
json_data = json_data + ',' + json.dumps(dic)
|
||||
|
||||
counter = counter + 1
|
||||
|
||||
json_data = json_data + ']'
|
||||
|
||||
totalEmail = emailList.emailsinlist_set.all().count()
|
||||
verified = emailList.verified
|
||||
notVerified = emailList.notVerified
|
||||
|
||||
data_ret = {'status': 1, 'logs': json_data, 'pagination': pagination, 'totalEmails': totalEmail, 'verified': verified, 'notVerified': notVerified}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
|
||||
|
||||
except BaseException as msg:
|
||||
data_ret = {'status': 0, 'error_message': str(msg)}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
|
||||
def saveConfigureVerify(self):
|
||||
try:
|
||||
|
||||
userID = self.request.session['userID']
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
|
||||
if emACL.checkIfEMEnabled(admin.userName) == 0:
|
||||
return ACLManager.loadErrorJson()
|
||||
|
||||
data = json.loads(self.request.body)
|
||||
|
||||
domain = data['domain']
|
||||
|
||||
configureVerifyPath = '/home/cyberpanel/configureVerify'
|
||||
|
||||
import os
|
||||
|
||||
if not os.path.exists(configureVerifyPath):
|
||||
os.mkdir(configureVerifyPath)
|
||||
|
||||
finalPath = '%s/%s' % (configureVerifyPath, domain)
|
||||
|
||||
writeToFile = open(finalPath, 'w')
|
||||
writeToFile.write(self.request.body.decode())
|
||||
writeToFile.close()
|
||||
|
||||
data_ret = {"status": 1}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
except BaseException as msg:
|
||||
final_dic = {'status': 0, 'error_message': str(msg)}
|
||||
final_json = json.dumps(final_dic)
|
||||
return HttpResponse(final_json)
|
||||
|
||||
def fetchEmails(self):
|
||||
try:
|
||||
|
||||
userID = self.request.session['userID']
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
|
||||
if emACL.checkIfEMEnabled(admin.userName) == 0:
|
||||
return ACLManager.loadErrorJson()
|
||||
|
||||
data = json.loads(self.request.body)
|
||||
|
||||
listName = data['listName']
|
||||
recordstoShow = int(data['recordstoShow'])
|
||||
page = int(data['page'])
|
||||
|
||||
finalPageNumber = ((page * recordstoShow)) - recordstoShow
|
||||
endPageNumber = finalPageNumber + recordstoShow
|
||||
|
||||
emailList = EmailLists.objects.get(listName=listName)
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
|
||||
if currentACL['admin'] == 1:
|
||||
pass
|
||||
elif emailList.owner.id != userID:
|
||||
return ACLManager.loadErrorJson()
|
||||
|
||||
emails = emailList.emailsinlist_set.all()
|
||||
|
||||
## Pagination value
|
||||
|
||||
pages = float(len(emails)) / float(recordstoShow)
|
||||
pagination = []
|
||||
counter = 1
|
||||
|
||||
if pages <= 1.0:
|
||||
pages = 1
|
||||
pagination.append(counter)
|
||||
else:
|
||||
pages = ceil(pages)
|
||||
finalPages = int(pages) + 1
|
||||
|
||||
for i in range(1, finalPages):
|
||||
pagination.append(counter)
|
||||
counter = counter + 1
|
||||
|
||||
## Pagination value
|
||||
|
||||
emails = emails[finalPageNumber:endPageNumber]
|
||||
|
||||
json_data = "["
|
||||
checker = 0
|
||||
counter = 1
|
||||
|
||||
for items in emails:
|
||||
|
||||
dic = {'id': items.id, 'email': items.email, 'verificationStatus': items.verificationStatus,
|
||||
'dateCreated': items.dateCreated}
|
||||
|
||||
if checker == 0:
|
||||
json_data = json_data + json.dumps(dic)
|
||||
checker = 1
|
||||
else:
|
||||
json_data = json_data + ',' + json.dumps(dic)
|
||||
|
||||
counter = counter + 1
|
||||
|
||||
json_data = json_data + ']'
|
||||
data_ret = {"status": 1, 'data': json_data, 'pagination': pagination}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
except BaseException as msg:
|
||||
final_dic = {'status': 0, 'error_message': str(msg)}
|
||||
final_json = json.dumps(final_dic)
|
||||
return HttpResponse(final_json)
|
||||
|
||||
def deleteList(self):
|
||||
try:
|
||||
|
||||
userID = self.request.session['userID']
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
|
||||
if emACL.checkIfEMEnabled(admin.userName) == 0:
|
||||
return ACLManager.loadErrorJson()
|
||||
|
||||
data = json.loads(self.request.body)
|
||||
|
||||
listName = data['listName']
|
||||
|
||||
delList = EmailLists.objects.get(listName=listName)
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
if currentACL['admin'] == 1:
|
||||
pass
|
||||
elif delList.owner.id != userID:
|
||||
return ACLManager.loadErrorJson()
|
||||
|
||||
delList.delete()
|
||||
|
||||
data_ret = {"status": 1}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
except BaseException as msg:
|
||||
final_dic = {'status': 0, 'error_message': str(msg)}
|
||||
final_json = json.dumps(final_dic)
|
||||
return HttpResponse(final_json)
|
||||
|
||||
def emailVerificationJob(self):
|
||||
try:
|
||||
|
||||
userID = self.request.session['userID']
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
|
||||
if emACL.checkIfEMEnabled(admin.userName) == 0:
|
||||
return ACLManager.loadErrorJson()
|
||||
|
||||
data = json.loads(self.request.body)
|
||||
extraArgs = {}
|
||||
extraArgs['listName'] = data['listName']
|
||||
|
||||
delList = EmailLists.objects.get(listName=extraArgs['listName'])
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
if currentACL['admin'] == 1:
|
||||
pass
|
||||
elif delList.owner.id != userID:
|
||||
return ACLManager.loadErrorJson()
|
||||
|
||||
em = EM('verificationJob', extraArgs)
|
||||
em.start()
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
data_ret = {"status": 1}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
except BaseException as msg:
|
||||
final_dic = {'status': 0, 'error_message': str(msg)}
|
||||
final_json = json.dumps(final_dic)
|
||||
return HttpResponse(final_json)
|
||||
|
||||
def deleteEmail(self):
|
||||
try:
|
||||
userID = self.request.session['userID']
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
|
||||
if emACL.checkIfEMEnabled(admin.userName) == 0:
|
||||
return ACLManager.loadErrorJson()
|
||||
|
||||
data = json.loads(self.request.body)
|
||||
|
||||
id = data['id']
|
||||
|
||||
delEmail = EmailsInList.objects.get(id=id)
|
||||
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
if currentACL['admin'] == 1:
|
||||
pass
|
||||
elif delEmail.owner.owner.id != userID:
|
||||
return ACLManager.loadErrorJson()
|
||||
|
||||
delEmail.delete()
|
||||
|
||||
data_ret = {"status": 1}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
except BaseException as msg:
|
||||
final_dic = {'status': 0, 'error_message': str(msg)}
|
||||
final_json = json.dumps(final_dic)
|
||||
return HttpResponse(final_json)
|
||||
|
||||
def manageSMTP(self):
|
||||
try:
|
||||
userID = self.request.session['userID']
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
|
||||
if ACLManager.checkOwnership(self.domain, admin, currentACL) == 1:
|
||||
pass
|
||||
else:
|
||||
return ACLManager.loadError()
|
||||
|
||||
if emACL.checkIfEMEnabled(admin.userName) == 0:
|
||||
return ACLManager.loadError()
|
||||
|
||||
website = Websites.objects.get(domain=self.domain)
|
||||
emailLists = website.emaillists_set.all()
|
||||
listNames = []
|
||||
|
||||
for items in emailLists:
|
||||
listNames.append(items.listName)
|
||||
|
||||
proc = httpProc(self.request, 'emailMarketing/manageSMTPHosts.html',
|
||||
{'listNames': listNames, 'domain': self.domain})
|
||||
return proc.render()
|
||||
except KeyError as msg:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
def saveSMTPHost(self):
|
||||
try:
|
||||
userID = self.request.session['userID']
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
|
||||
if emACL.checkIfEMEnabled(admin.userName) == 0:
|
||||
return ACLManager.loadErrorJson()
|
||||
|
||||
|
||||
data = json.loads(self.request.body)
|
||||
|
||||
smtpHost = data['smtpHost']
|
||||
smtpPort = data['smtpPort']
|
||||
smtpUserName = data['smtpUserName']
|
||||
smtpPassword = data['smtpPassword']
|
||||
|
||||
if SMTPHosts.objects.count() == 0:
|
||||
admin = Administrator.objects.get(userName='admin')
|
||||
defaultHost = SMTPHosts(owner=admin, host='localhost', port=25, userName='None', password='None')
|
||||
defaultHost.save()
|
||||
|
||||
try:
|
||||
verifyLogin = smtplib.SMTP(str(smtpHost), int(smtpPort))
|
||||
|
||||
if int(smtpPort) == 587:
|
||||
verifyLogin.starttls()
|
||||
|
||||
verifyLogin.login(str(smtpUserName), str(smtpPassword))
|
||||
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
|
||||
newHost = SMTPHosts(owner=admin, host=smtpHost, port=smtpPort, userName=smtpUserName,
|
||||
password=smtpPassword)
|
||||
newHost.save()
|
||||
|
||||
except smtplib.SMTPHeloError:
|
||||
data_ret = {"status": 0, 'error_message': 'The server did not reply properly to the HELO greeting.'}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
except smtplib.SMTPAuthenticationError:
|
||||
data_ret = {"status": 0, 'error_message': 'Username and password combination not accepted.'}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
except smtplib.SMTPException:
|
||||
data_ret = {"status": 0, 'error_message': 'No suitable authentication method was found.'}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
|
||||
data_ret = {"status": 1}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
except BaseException as msg:
|
||||
final_dic = {'status': 0, 'error_message': str(msg)}
|
||||
final_json = json.dumps(final_dic)
|
||||
return HttpResponse(final_json)
|
||||
|
||||
def fetchSMTPHosts(self):
|
||||
try:
|
||||
|
||||
userID = self.request.session['userID']
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
|
||||
if emACL.checkIfEMEnabled(admin.userName) == 0:
|
||||
return ACLManager.loadErrorJson()
|
||||
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
|
||||
if currentACL['admin'] == 1:
|
||||
allHosts = SMTPHosts.objects.all()
|
||||
else:
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
allHosts = admin.smtphosts_set.all()
|
||||
|
||||
json_data = "["
|
||||
checker = 0
|
||||
counter = 1
|
||||
|
||||
for items in allHosts:
|
||||
|
||||
dic = {'id': items.id, 'owner': items.owner.userName, 'host': items.host, 'port': items.port,
|
||||
'userName': items.userName}
|
||||
|
||||
if checker == 0:
|
||||
json_data = json_data + json.dumps(dic)
|
||||
checker = 1
|
||||
else:
|
||||
json_data = json_data + ',' + json.dumps(dic)
|
||||
|
||||
counter = counter + 1
|
||||
|
||||
json_data = json_data + ']'
|
||||
data_ret = {"status": 1, 'data': json_data}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
except BaseException as msg:
|
||||
final_dic = {'status': 0, 'error_message': str(msg)}
|
||||
final_json = json.dumps(final_dic)
|
||||
return HttpResponse(final_json)
|
||||
|
||||
def smtpHostOperations(self):
|
||||
try:
|
||||
|
||||
userID = self.request.session['userID']
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
|
||||
if emACL.checkIfEMEnabled(admin.userName) == 0:
|
||||
return ACLManager.loadErrorJson()
|
||||
|
||||
data = json.loads(self.request.body)
|
||||
|
||||
id = data['id']
|
||||
operation = data['operation']
|
||||
|
||||
if operation == 'delete':
|
||||
delHost = SMTPHosts.objects.get(id=id)
|
||||
|
||||
if ACLManager.VerifySMTPHost(currentACL, delHost.owner, admin) == 0:
|
||||
return ACLManager.loadErrorJson()
|
||||
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
if currentACL['admin'] == 1:
|
||||
pass
|
||||
elif delHost.owner.id != userID:
|
||||
return ACLManager.loadErrorJson()
|
||||
delHost.delete()
|
||||
data_ret = {"status": 1, 'message': 'Successfully deleted.'}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
else:
|
||||
try:
|
||||
verifyHost = SMTPHosts.objects.get(id=id)
|
||||
|
||||
if ACLManager.VerifySMTPHost(currentACL, verifyHost.owner, admin) == 0:
|
||||
return ACLManager.loadErrorJson()
|
||||
|
||||
verifyLogin = smtplib.SMTP(str(verifyHost.host), int(verifyHost.port))
|
||||
|
||||
if int(verifyHost.port) == 587:
|
||||
verifyLogin.starttls()
|
||||
|
||||
verifyLogin.login(str(verifyHost.userName), str(verifyHost.password))
|
||||
|
||||
data_ret = {"status": 1, 'message': 'Login successful.'}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
except smtplib.SMTPHeloError:
|
||||
data_ret = {"status": 0, 'error_message': 'The server did not reply properly to the HELO greeting.'}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
except smtplib.SMTPAuthenticationError:
|
||||
data_ret = {"status": 0, 'error_message': 'Username and password combination not accepted.'}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
except smtplib.SMTPException:
|
||||
data_ret = {"status": 0, 'error_message': 'No suitable authentication method was found.'}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
|
||||
except BaseException as msg:
|
||||
final_dic = {'status': 0, 'error_message': str(msg)}
|
||||
final_json = json.dumps(final_dic)
|
||||
return HttpResponse(final_json)
|
||||
|
||||
def composeEmailMessage(self):
|
||||
try:
|
||||
userID = self.request.session['userID']
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
|
||||
if emACL.checkIfEMEnabled(admin.userName) == 0:
|
||||
return ACLManager.loadErrorJson()
|
||||
|
||||
proc = httpProc(self.request, 'emailMarketing/composeMessages.html',
|
||||
None)
|
||||
return proc.render()
|
||||
except KeyError as msg:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
def saveEmailTemplate(self):
|
||||
try:
|
||||
userID = self.request.session['userID']
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
|
||||
if emACL.checkIfEMEnabled(admin.userName) == 0:
|
||||
return ACLManager.loadErrorJson()
|
||||
|
||||
data = json.loads(self.request.body)
|
||||
|
||||
name = data['name']
|
||||
subject = data['subject']
|
||||
fromName = data['fromName']
|
||||
fromEmail = data['fromEmail']
|
||||
replyTo = data['replyTo']
|
||||
emailMessage = data['emailMessage']
|
||||
|
||||
if ACLManager.CheckRegEx('[\w\d\s]+$', name) == 0:
|
||||
return ACLManager.loadErrorJson()
|
||||
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
newTemplate = EmailTemplate(owner=admin, name=name.replace(' ', ''), subject=subject, fromName=fromName, fromEmail=fromEmail,
|
||||
replyTo=replyTo, emailMessage=emailMessage)
|
||||
newTemplate.save()
|
||||
|
||||
data_ret = {"status": 1}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
except BaseException as msg:
|
||||
final_dic = {'status': 0, 'error_message': str(msg)}
|
||||
final_json = json.dumps(final_dic)
|
||||
return HttpResponse(final_json)
|
||||
|
||||
def sendEmails(self):
|
||||
try:
|
||||
userID = self.request.session['userID']
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
|
||||
if emACL.checkIfEMEnabled(admin.userName) == 0:
|
||||
return ACLManager.loadErrorJson()
|
||||
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
templateNames = emACL.allTemplates(currentACL, admin)
|
||||
hostNames = emACL.allSMTPHosts(currentACL, admin)
|
||||
listNames = emACL.allEmailsLists(currentACL, admin)
|
||||
|
||||
|
||||
Data = {}
|
||||
Data['templateNames'] = templateNames
|
||||
Data['hostNames'] = hostNames
|
||||
Data['listNames'] = listNames
|
||||
|
||||
proc = httpProc(self.request, 'emailMarketing/sendEmails.html',
|
||||
Data)
|
||||
return proc.render()
|
||||
|
||||
except KeyError as msg:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
def templatePreview(self):
|
||||
try:
|
||||
userID = self.request.session['userID']
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
template = EmailTemplate.objects.get(name=self.domain)
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
|
||||
if currentACL['admin'] == 1:
|
||||
pass
|
||||
elif template.owner != admin:
|
||||
return ACLManager.loadError()
|
||||
|
||||
return HttpResponse(template.emailMessage)
|
||||
except KeyError as msg:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
def fetchJobs(self):
|
||||
try:
|
||||
|
||||
userID = self.request.session['userID']
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
|
||||
if emACL.checkIfEMEnabled(admin.userName) == 0:
|
||||
return ACLManager.loadErrorJson()
|
||||
|
||||
data = json.loads(self.request.body)
|
||||
selectedTemplate = data['selectedTemplate']
|
||||
|
||||
template = EmailTemplate.objects.get(name=selectedTemplate)
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
|
||||
if currentACL['admin'] == 1:
|
||||
pass
|
||||
elif template.owner != admin:
|
||||
return ACLManager.loadErrorJson()
|
||||
|
||||
allJobs = EmailJobs.objects.filter(owner=template)
|
||||
|
||||
json_data = "["
|
||||
checker = 0
|
||||
counter = 1
|
||||
|
||||
for items in allJobs:
|
||||
|
||||
dic = {'id': items.id,
|
||||
'date': items.date,
|
||||
'host': items.host,
|
||||
'totalEmails': items.totalEmails,
|
||||
'sent': items.sent,
|
||||
'failed': items.failed}
|
||||
|
||||
if checker == 0:
|
||||
json_data = json_data + json.dumps(dic)
|
||||
checker = 1
|
||||
else:
|
||||
json_data = json_data + ',' + json.dumps(dic)
|
||||
|
||||
counter = counter + 1
|
||||
|
||||
json_data = json_data + ']'
|
||||
data_ret = {"status": 1, 'data': json_data}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
except BaseException as msg:
|
||||
final_dic = {'status': 0, 'error_message': str(msg)}
|
||||
final_json = json.dumps(final_dic)
|
||||
return HttpResponse(final_json)
|
||||
|
||||
def startEmailJob(self):
|
||||
try:
|
||||
userID = self.request.session['userID']
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
data = json.loads(self.request.body)
|
||||
|
||||
extraArgs = {}
|
||||
extraArgs['selectedTemplate'] = data['selectedTemplate']
|
||||
extraArgs['listName'] = data['listName']
|
||||
extraArgs['host'] = data['host']
|
||||
try:
|
||||
extraArgs['verificationCheck'] = data['verificationCheck']
|
||||
except:
|
||||
extraArgs['verificationCheck'] = False
|
||||
try:
|
||||
extraArgs['unsubscribeCheck'] = data['unsubscribeCheck']
|
||||
except:
|
||||
extraArgs['unsubscribeCheck'] = False
|
||||
|
||||
extraArgs['tempStatusPath'] = "/home/cyberpanel/" + data['selectedTemplate'] + '_pendingJob'
|
||||
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
template = EmailTemplate.objects.get(name=extraArgs['selectedTemplate'])
|
||||
|
||||
if currentACL['admin'] == 1:
|
||||
pass
|
||||
elif template.owner != admin:
|
||||
return ACLManager.loadErrorJson()
|
||||
|
||||
em = EM('startEmailJob', extraArgs)
|
||||
em.start()
|
||||
|
||||
time.sleep(5)
|
||||
|
||||
data_ret = {"status": 1, 'tempStatusPath': extraArgs['tempStatusPath']}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
except BaseException as msg:
|
||||
final_dic = {'status': 0, 'error_message': str(msg)}
|
||||
final_json = json.dumps(final_dic)
|
||||
return HttpResponse(final_json)
|
||||
|
||||
def deleteTemplate(self):
|
||||
try:
|
||||
userID = self.request.session['userID']
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
data = json.loads(self.request.body)
|
||||
|
||||
selectedTemplate = data['selectedTemplate']
|
||||
|
||||
delTemplate = EmailTemplate.objects.get(name=selectedTemplate)
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
|
||||
if currentACL['admin'] == 1:
|
||||
pass
|
||||
elif delTemplate.owner != admin:
|
||||
return ACLManager.loadErrorJson()
|
||||
delTemplate.delete()
|
||||
|
||||
data_ret = {"status": 1}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
except BaseException as msg:
|
||||
final_dic = {'status': 0, 'error_message': str(msg)}
|
||||
final_json = json.dumps(final_dic)
|
||||
return HttpResponse(final_json)
|
||||
|
||||
def deleteJob(self):
|
||||
try:
|
||||
userID = self.request.session['userID']
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
|
||||
if emACL.checkIfEMEnabled(admin.userName) == 0:
|
||||
return ACLManager.loadErrorJson()
|
||||
|
||||
data = json.loads(self.request.body)
|
||||
id = data['id']
|
||||
delJob = EmailJobs(id=id)
|
||||
delJob.delete()
|
||||
data_ret = {"status": 1}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
except BaseException as msg:
|
||||
final_dic = {'status': 0, 'error_message': str(msg)}
|
||||
final_json = json.dumps(final_dic)
|
||||
return HttpResponse(final_json)
|
||||
|
||||
def remove(self, listName, emailAddress):
|
||||
try:
|
||||
eList = EmailLists.objects.get(listName=listName)
|
||||
removeEmail = EmailsInList.objects.get(owner=eList, email=emailAddress)
|
||||
removeEmail.verificationStatus = 'REMOVED'
|
||||
removeEmail.save()
|
||||
except:
|
||||
pass
|
||||
|
||||
return HttpResponse('Email Address Successfully removed from the list.')
|
||||
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<cyberpanelPluginConfig>
|
||||
<name>Email Marketing</name>
|
||||
<type>plugin</type>
|
||||
<description>Email Marketing plugin for CyberPanel.</description>
|
||||
<version>1.0.0</version>
|
||||
</cyberpanelPluginConfig>
|
||||
@@ -1,56 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from django.db import models
|
||||
from websiteFunctions.models import Websites
|
||||
from loginSystem.models import Administrator
|
||||
|
||||
# Create your models here.
|
||||
|
||||
class EmailMarketing(models.Model):
|
||||
userName = models.CharField(max_length=50, unique=True)
|
||||
|
||||
class EmailLists(models.Model):
|
||||
owner = models.ForeignKey(Websites, on_delete=models.PROTECT)
|
||||
listName = models.CharField(max_length=50, unique=True)
|
||||
dateCreated = models.CharField(max_length=200)
|
||||
verified = models.IntegerField(default=0)
|
||||
notVerified = models.IntegerField(default=0)
|
||||
|
||||
class EmailsInList(models.Model):
|
||||
owner = models.ForeignKey(EmailLists, on_delete=models.CASCADE)
|
||||
email = models.CharField(max_length=50)
|
||||
firstName = models.CharField(max_length=20, default='')
|
||||
lastName = models.CharField(max_length=20, default='')
|
||||
verificationStatus = models.CharField(max_length=100)
|
||||
dateCreated = models.CharField(max_length=200)
|
||||
|
||||
class SMTPHosts(models.Model):
|
||||
owner = models.ForeignKey(Administrator, on_delete=models.CASCADE)
|
||||
host = models.CharField(max_length=150, unique= True)
|
||||
port = models.CharField(max_length=10)
|
||||
userName = models.CharField(max_length=200)
|
||||
password = models.CharField(max_length=200)
|
||||
|
||||
class EmailTemplate(models.Model):
|
||||
owner = models.ForeignKey(Administrator, on_delete=models.CASCADE)
|
||||
name = models.CharField(unique=True, max_length=100)
|
||||
subject = models.CharField(max_length=1000)
|
||||
fromName = models.CharField(max_length=100)
|
||||
fromEmail = models.CharField(max_length=150)
|
||||
replyTo = models.CharField(max_length=150)
|
||||
emailMessage = models.TextField(max_length=65532)
|
||||
|
||||
class EmailJobs(models.Model):
|
||||
owner = models.ForeignKey(EmailTemplate, on_delete=models.CASCADE)
|
||||
date = models.CharField(max_length=200)
|
||||
host = models.CharField(max_length=1000)
|
||||
totalEmails = models.IntegerField()
|
||||
sent = models.IntegerField()
|
||||
failed = models.IntegerField()
|
||||
|
||||
class ValidationLog(models.Model):
|
||||
owner = models.ForeignKey(EmailLists, on_delete=models.CASCADE)
|
||||
status = models.IntegerField()
|
||||
message = models.TextField()
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.8 KiB |
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,90 +0,0 @@
|
||||
{% extends "baseTemplate/index.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Compose Email Message - CyberPanel" %}{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
{% load static %}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
<!-- Current language: {{ LANGUAGE_CODE }} -->
|
||||
|
||||
|
||||
<div class="container">
|
||||
<div id="page-title">
|
||||
<h2>{% trans "Compose Email Message" %}</h2>
|
||||
<p>{% trans "On this page you can compose email message to be sent out later." %}</p>
|
||||
</div>
|
||||
<div ng-controller="composeMessageCTRL" class="panel" style="background: var(--bg-primary, white); border-color: var(--border-color, #ddd);">
|
||||
<div class="panel-body" style="background: var(--bg-primary, white); color: var(--text-primary, #333);">
|
||||
<h3 class="title-hero">
|
||||
{% trans "Compose Email Message" %} <img ng-hide="cyberPanelLoading" src="{% static 'images/loading.gif' %}">
|
||||
</h3>
|
||||
<div class="example-box-wrapper">
|
||||
|
||||
|
||||
<form action="/" class="form-horizontal bordered-row">
|
||||
|
||||
<!---- Create Email Template --->
|
||||
|
||||
<div ng-hide="installationDetailsForm" class="form-group">
|
||||
<label class="col-sm-3 control-label">{% trans "Template Name" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" class="form-control" ng-model="name" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-hide="installationDetailsForm" class="form-group">
|
||||
<label class="col-sm-3 control-label">{% trans "Email Subject" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" class="form-control" ng-model="subject" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-hide="installationDetailsForm" class="form-group">
|
||||
<label class="col-sm-3 control-label">{% trans "From Name" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" class="form-control" ng-model="fromName" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-hide="installationDetailsForm" class="form-group">
|
||||
<label class="col-sm-3 control-label">{% trans "From Email" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" class="form-control" ng-model="fromEmail" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-hide="installationDetailsForm" class="form-group">
|
||||
<label class="col-sm-3 control-label">{% trans "Reply Email" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" class="form-control" ng-model="replyTo" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div ng-hide="request" class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<textarea placeholder="Paste your email message, any format is accepted. (HTML or Plain)" ng-model="emailMessage" rows="15" class="form-control"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-hide="installationProgress" class="form-group">
|
||||
<label class="col-sm-3 control-label"></label>
|
||||
<div class="col-sm-4">
|
||||
<button type="button" ng-click="saveTemplate()" class="btn btn-primary btn-lg btn-block">{% trans "Save Template" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!---- Create Email Template --->
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,99 +0,0 @@
|
||||
{% extends "baseTemplate/index.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Configure Email Verification - CyberPanel" %}{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
{% load static %}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
<!-- Current language: {{ LANGUAGE_CODE }} -->
|
||||
|
||||
|
||||
<div class="container">
|
||||
<div id="page-title">
|
||||
<h2>{% trans "Configure Email Verification" %}</h2>
|
||||
<p>{% trans "On this page you can configure parameters regarding how email verification is performed for " %}<span id="domainName">{{ domain }}</span></p>
|
||||
</div>
|
||||
<div ng-controller="configureVerify" class="panel" style="background: var(--bg-primary, white); border-color: var(--border-color, #ddd);">
|
||||
<div class="panel-body" style="background: var(--bg-primary, white); color: var(--text-primary, #333);">
|
||||
<h3 class="title-hero">
|
||||
{% trans "Compose Email Message" %} <img ng-hide="cyberPanelLoading"
|
||||
src="{% static 'images/loading.gif' %}">
|
||||
</h3>
|
||||
<div class="example-box-wrapper">
|
||||
|
||||
|
||||
<form action="/" class="form-horizontal bordered-row">
|
||||
|
||||
<!---- Create Email Template --->
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">{% trans "Configure Delay" %} </label>
|
||||
<div class="col-sm-6">
|
||||
<select ng-change="delayInitial()" ng-model="delay" class="form-control">
|
||||
<option>Disable</option>
|
||||
<option>Enable</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-hide="delayHidden" class="form-group">
|
||||
<label class="col-sm-3 control-label">{% trans "Delay After" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input placeholder="{% trans 'Start delay after this many verifications are done.' %}" type="number" class="form-control" ng-model="delayAfter" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-hide="delayHidden" class="form-group">
|
||||
<label class="col-sm-3 control-label">{% trans "Delay Time" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input placeholder="{% trans 'Set the number of seconds to wait.' %}" type="number" class="form-control" ng-model="delayTime" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">{% trans "IP Rotation" %} </label>
|
||||
<div class="col-sm-6">
|
||||
<select ng-change="rotateInitial()" ng-model="rotation" class="form-control">
|
||||
<option>Disable</option>
|
||||
<option>IPv4</option>
|
||||
<option>IPv6</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-hide="ipv4Hidden" class="form-group">
|
||||
<label class="col-sm-3 control-label">{% trans "IPv4" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input placeholder="{% trans 'Enter IPv4(s) to be used separate with commas.' %}" type="text" class="form-control" ng-model="ipv4" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-hide="ipv6Hidden" class="form-group">
|
||||
<label class="col-sm-3 control-label">{% trans "IPv6" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input placeholder="{% trans 'Enter IPv6(s) to be used separate with commas.' %}" type="text" class="form-control" ng-model="ipv6" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div ng-hide="installationProgress" class="form-group">
|
||||
<label class="col-sm-3 control-label"></label>
|
||||
<div class="col-sm-4">
|
||||
<button type="button" ng-click="saveChanges()"
|
||||
class="btn btn-primary btn-lg btn-block">{% trans "Save" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!---- Create Email Template --->
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,75 +0,0 @@
|
||||
{% extends "baseTemplate/index.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Create Email List - CyberPanel" %}{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
{% load static %}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
<!-- Current language: {{ LANGUAGE_CODE }} -->
|
||||
|
||||
|
||||
<div ng-controller="createEmailList" class="container">
|
||||
<div id="page-title">
|
||||
<h2>{% trans "Create Email List" %} - <span id="domainNamePage">{{ domain }}</span> </h2>
|
||||
<p>{% trans "Create email list, to send out news letters and marketing emails." %}</p>
|
||||
</div>
|
||||
<div class="panel" style="background: var(--bg-primary, white); border-color: var(--border-color, #ddd);">
|
||||
<div class="panel-body" style="background: var(--bg-primary, white); color: var(--text-primary, #333);">
|
||||
<h3 class="title-hero">
|
||||
{% trans "Create Email List" %} <img ng-hide="cyberPanelLoading" src="{% static 'images/loading.gif' %}">
|
||||
</h3>
|
||||
<div class="example-box-wrapper">
|
||||
|
||||
|
||||
<form action="/" id="createPackages" class="form-horizontal bordered-row">
|
||||
<div ng-hide="installationDetailsForm" class="form-group">
|
||||
<label class="col-sm-3 control-label">{% trans "List Name" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input name="pname" type="text" class="form-control" ng-model="listName" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-hide="installationDetailsForm" class="form-group">
|
||||
<label class="col-sm-3 control-label">{% trans "Path" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input placeholder="Path to emails file (.txt and .csv accepted)" type="text" class="form-control" ng-model="path" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-hide="installationDetailsForm" class="form-group">
|
||||
<label class="col-sm-3 control-label"></label>
|
||||
<div class="col-sm-4">
|
||||
<button type="button" ng-click="createEmailList()" class="btn btn-primary btn-lg btn-block">{% trans "Create List" %}</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-hide="installationProgress" class="form-group">
|
||||
<label class="col-sm-2 control-label"></label>
|
||||
<div class="col-sm-7">
|
||||
<div class="alert alert-success text-center" style="background: var(--bg-secondary, #f8f9ff); color: var(--text-primary, #333); border-color: var(--border-color, #ddd);">
|
||||
<h2>{$ currentStatus $}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-hide="installationProgress" class="form-group">
|
||||
<label class="col-sm-3 control-label"></label>
|
||||
<div class="col-sm-4">
|
||||
<button type="button" ng-disabled="goBackDisable" ng-click="goBack()" class="btn btn-primary btn-lg btn-block">{% trans "Go Back" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
{% extends "baseTemplate/index.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Email Marketing - CyberPanel" %}{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
{% load static %}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
<!-- Current language: {{ LANGUAGE_CODE }} -->
|
||||
|
||||
|
||||
<div class="container">
|
||||
|
||||
<div id="page-title">
|
||||
<h2 id="domainNamePage">{% trans "Email Marketing" %}</h2>
|
||||
<p>{% trans "Select users to Enable/Disable Email Marketing feature!" %}</p>
|
||||
</div>
|
||||
|
||||
<div ng-controller="emailMarketing" class="panel" style="background: var(--bg-primary, white); border-color: var(--border-color, #ddd);">
|
||||
<div class="panel-body" style="background: var(--bg-primary, white); color: var(--text-primary, #333);">
|
||||
|
||||
<h3 class="content-box-header">
|
||||
{% trans "Email Marketing" %} <img ng-hide="cyberPanelLoading" src="{% static 'images/loading.gif' %}">
|
||||
</h3>
|
||||
|
||||
{% if installCheck == 0 %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center" style="margin-bottom: 2%;">
|
||||
<h3>{% trans "Email Policy Server is not enabled " %}
|
||||
<a href="{% url 'emailPolicyServer' %}">
|
||||
<button class="btn btn-alt btn-hover btn-blue-alt">
|
||||
<span>{% trans "Enable Now." %}</span>
|
||||
<i class="glyph-icon icon-arrow-right"></i>
|
||||
</button></a></h3>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="example-box-wrapper">
|
||||
|
||||
<table cellpadding="0" cellspacing="0" border="0" class="table table-striped" id="datatable-example" style="background: var(--bg-primary, white); color: var(--text-primary, #333); border-color: var(--border-color, #ddd);">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans 'ID' %}</th>
|
||||
<th>{% trans 'Username' %}</th>
|
||||
<th>{% trans 'Status' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
<tr ng-repeat="user in users track by $index">
|
||||
<td ><code ng-bind="user.id"></code></td>
|
||||
<td><code ng-bind="user.userName"></code></td>
|
||||
<td>
|
||||
<img style="margin-right: 4%;" ng-show="user.status==1" title="{% trans 'Email Marketing Enabled.' %}" src="{% static 'mailServer/vpsON.png' %}">
|
||||
<button ng-click="enableDisableMarketing(0, user.userName)" ng-show="user.status==1" class="btn ra-100 btn-danger">{% trans 'Disable' %}</button>
|
||||
<img style="margin-right: 4%;" ng-show="user.status==0" title="{% trans 'Email Marketing Disabled.' %}" src="{% static 'mailServer/vpsOff.png' %}">
|
||||
<button ng-click="enableDisableMarketing(1, user.userName)" ng-show="user.status==0" class="btn ra-100 btn-success">{% trans 'Enable' %}</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,315 +0,0 @@
|
||||
{% extends "baseTemplate/index.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Manage Email Lists - CyberPanel" %}{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
{% load static %}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
<!-- Current language: {{ LANGUAGE_CODE }} -->
|
||||
|
||||
|
||||
<div class="container">
|
||||
<div id="page-title">
|
||||
<h2>{% trans "Manage Email Lists" %} - <span id="domainNamePage">{{ domain }}</span></h2>
|
||||
<p>{% trans "On this page you can manage your email lists (Delete, Verify, Add More Emails)." %}</p>
|
||||
</div>
|
||||
<div ng-controller="manageEmailLists" class="panel" style="background: var(--bg-primary, white); border-color: var(--border-color, #ddd);">
|
||||
<div class="panel-body" style="background: var(--bg-primary, white); color: var(--text-primary, #333);">
|
||||
<h3 class="title-hero">
|
||||
{% trans "Manage Email Lists" %} <img ng-hide="cyberPanelLoading"
|
||||
src="{% static 'images/loading.gif' %}">
|
||||
</h3>
|
||||
<div class="example-box-wrapper">
|
||||
|
||||
|
||||
<form action="/" class="form-horizontal bordered-row">
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">{% trans "Select List" %} </label>
|
||||
<div class="col-sm-6">
|
||||
<select ng-change="fetchEmails(1)" ng-model="listName" class="form-control">
|
||||
{% for items in listNames %}
|
||||
<option>{{ items }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-hide="currentRecords" class="form-group">
|
||||
<div class="row">
|
||||
<div class="col-sm-1">
|
||||
<button data-toggle="modal" data-target="#deleteList"
|
||||
class="btn ra-100 btn-danger">{% trans 'Delete' %}</button>
|
||||
<!--- Delete Pool --->
|
||||
<div class="modal fade" id="deleteList" tabindex="-1" role="dialog"
|
||||
aria-labelledby="myModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content" style="background: var(--bg-primary, white); color: var(--text-primary, #333); border-color: var(--border-color, #ddd);">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal"
|
||||
aria-hidden="true">×
|
||||
</button>
|
||||
<h4 class="modal-title">{% trans "You are doing to delete this list.." %}
|
||||
<img ng-hide="cyberPanelLoading"
|
||||
src="{% static 'images/loading.gif' %}"></h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{% trans 'Are you sure?' %}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default"
|
||||
data-dismiss="modal">{% trans 'Close' %}</button>
|
||||
<button data-dismiss="modal" ng-click="deleteList()" type="button"
|
||||
class="btn btn-primary">{% trans 'Confirm' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--- Delete Pool --->
|
||||
</div>
|
||||
<div class="col-sm-1">
|
||||
<button ng-disabled="verificationButton" ng-click="startVerification()"
|
||||
class="btn ra-100 btn-blue-alt">{% trans 'Verify' %}</button>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<button onclick="location.href='/emailMarketing/{{ domain }}/configureVerify'"
|
||||
class="btn ra-100 btn-blue-alt">{% trans 'Configure Verification' %}</button>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<button ng-click="fetchLogs()" data-toggle="modal" data-target="#verificationLogs"
|
||||
class="btn ra-100 btn-blue-alt">{% trans 'Verfications Logs' %}</button>
|
||||
<!--- Delete Pool --->
|
||||
<div class="modal fade" id="verificationLogs" tabindex="-1" role="dialog"
|
||||
aria-labelledby="myModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content" style="background: var(--bg-primary, white); color: var(--text-primary, #333); border-color: var(--border-color, #ddd);">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal"
|
||||
aria-hidden="true">×
|
||||
</button>
|
||||
<h4 class="modal-title">{% trans "Verification Logs" %}
|
||||
<img ng-hide="cyberPanelLoading"
|
||||
src="{% static 'images/loading.gif' %}"></h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!------ List of records --------------->
|
||||
|
||||
<div ng-hide="currentRecords" class="form-group">
|
||||
|
||||
<table style="margin: 0px; padding-bottom: 2%; background: var(--bg-primary, white); color: var(--text-primary, #333); border-color: var(--border-color, #ddd);" class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Total Emails" %}</th>
|
||||
<th>{% trans "Verified" %}</th>
|
||||
<th>{% trans "Not-Verified" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{$ totalEmails $}</td>
|
||||
<td>{$ verified $}</td>
|
||||
<td>{$ notVerified $}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="col-sm-10">
|
||||
<input placeholder="Search Logs..." name="dom" type="text"
|
||||
class="form-control" ng-model="searchLogs"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 1%;" class="col-sm-2">
|
||||
<select ng-change="fetchLogs()" ng-model="recordsToShowLogs"
|
||||
class="form-control">
|
||||
<option>10</option>
|
||||
<option>50</option>
|
||||
<option>100</option>
|
||||
<option>500</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12">
|
||||
|
||||
<table style="margin: 0px; background: var(--bg-primary, white); color: var(--text-primary, #333); border-color: var(--border-color, #ddd);" class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Status" %}</th>
|
||||
<th>{% trans "Message" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="record in recordsLogs | filter:searchLogs">
|
||||
<td ng-bind="record.status"></td>
|
||||
<td ng-bind="record.message"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div style="margin-top: 2%" class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<select ng-model="currentPageLogs"
|
||||
class="form-control"
|
||||
ng-change="fetchLogs()">
|
||||
<option ng-repeat="page in paginationLogs">
|
||||
{$ $index + 1
|
||||
$}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- end row -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!------ List of records --------------->
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default"
|
||||
data-dismiss="modal">{% trans 'Close' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--- Delete Pool --->
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<button ng-click="showAddEmails()"
|
||||
class="btn ra-100 btn-blue-alt">{% trans 'Add More Emails' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!---- Create Email List --->
|
||||
|
||||
<div ng-hide="installationDetailsForm" class="form-group">
|
||||
<label class="col-sm-3 control-label">{% trans "Path" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input placeholder="Path to emails file (.txt and .csv accepted)" type="text"
|
||||
class="form-control" ng-model="path" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-hide="installationDetailsForm" class="form-group">
|
||||
<label class="col-sm-3 control-label"></label>
|
||||
<div class="col-sm-4">
|
||||
<button type="button" ng-click="createEmailList()"
|
||||
class="btn btn-primary btn-lg btn-block">{% trans "Load Emails" %}</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-hide="installationProgress" class="form-group">
|
||||
<label class="col-sm-2 control-label"></label>
|
||||
<div class="col-sm-7">
|
||||
<div class="alert alert-success text-center" style="background: var(--bg-secondary, #f8f9ff); color: var(--text-primary, #333); border-color: var(--border-color, #ddd);">
|
||||
<h2>{$ currentStatus $}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-hide="installationProgress" class="form-group">
|
||||
<label class="col-sm-3 control-label"></label>
|
||||
<div class="col-sm-4">
|
||||
<button type="button" ng-disabled="goBackDisable" ng-click="goBack()"
|
||||
class="btn btn-primary btn-lg btn-block">{% trans "Go Back" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!---- Create Email List --->
|
||||
|
||||
<!---- Email List Verification --->
|
||||
|
||||
<div ng-hide="verificationStatus" class="form-group">
|
||||
<label class="col-sm-2 control-label"></label>
|
||||
<div class="col-sm-7">
|
||||
<div class="alert alert-success text-center" style="background: var(--bg-secondary, #f8f9ff); color: var(--text-primary, #333); border-color: var(--border-color, #ddd);">
|
||||
<h2>{$ currentStatusVerification $}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!---- Create Email List --->
|
||||
|
||||
|
||||
<!------ List of records --------------->
|
||||
|
||||
<div ng-hide="currentRecords" class="form-group">
|
||||
|
||||
<div class="col-sm-10">
|
||||
<input placeholder="Search Emails..." ng-model="searchEmails" name="dom" type="text"
|
||||
class="form-control" ng-model="domainNameCreate" required>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 1%;" class="col-sm-2">
|
||||
<select ng-change="fetchRecords()" ng-model="recordstoShow" class="form-control">
|
||||
<option>10</option>
|
||||
<option>50</option>
|
||||
<option>100</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12">
|
||||
|
||||
<table class="table" style="background: var(--bg-primary, white); color: var(--text-primary, #333); border-color: var(--border-color, #ddd);">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "ID" %}</th>
|
||||
<th>{% trans "email" %}</th>
|
||||
<th>{% trans "Verification Status" %}</th>
|
||||
<th>{% trans "Date Created" %}</th>
|
||||
<th>{% trans "Actions" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="record in records | filter:searchEmails">
|
||||
<td ng-bind="record.id"></td>
|
||||
<td ng-bind="record.email"></td>
|
||||
<td ng-bind="record.verificationStatus"></td>
|
||||
<td ng-bind="record.dateCreated"></td>
|
||||
<td>
|
||||
<button type="button" ng-click="deleteEmail(record.id)"
|
||||
class="btn ra-100 btn-purple">{% trans "Delete" %}</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-4 col-sm-offset-8">
|
||||
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination">
|
||||
<li ng-click="fetchEmails(page)" ng-repeat="page in pagination"><a
|
||||
href="">{$ page $}</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!------ List of records --------------->
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,126 +0,0 @@
|
||||
{% extends "baseTemplate/index.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Manage SMTP Hosts - CyberPanel" %}{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
{% load static %}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
<!-- Current language: {{ LANGUAGE_CODE }} -->
|
||||
|
||||
|
||||
<div class="container">
|
||||
<div id="page-title">
|
||||
<h2>{% trans "Manage SMTP Hosts" %}</h2>
|
||||
<p>{% trans "On this page you can manage STMP Host. (SMTP hosts are used to send emails)" %}</p>
|
||||
</div>
|
||||
<div ng-controller="manageSMTPHostsCTRL" class="panel" style="background: var(--bg-primary, white); border-color: var(--border-color, #ddd);">
|
||||
<div class="panel-body" style="background: var(--bg-primary, white); color: var(--text-primary, #333);">
|
||||
<h3 class="title-hero">
|
||||
{% trans "Manage SMTP Hosts" %} <img ng-hide="cyberPanelLoading" src="{% static 'images/loading.gif' %}">
|
||||
</h3>
|
||||
<div class="example-box-wrapper">
|
||||
|
||||
|
||||
<form action="/" class="form-horizontal bordered-row">
|
||||
|
||||
<!---- Create SMTP Host --->
|
||||
|
||||
<div ng-hide="installationDetailsForm" class="form-group">
|
||||
<label class="col-sm-3 control-label">{% trans "SMTP Host" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" class="form-control" ng-model="smtpHost" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-hide="installationDetailsForm" class="form-group">
|
||||
<label class="col-sm-3 control-label">{% trans "Port" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" class="form-control" ng-model="smtpPort" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-hide="installationDetailsForm" class="form-group">
|
||||
<label class="col-sm-3 control-label">{% trans "Username" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" class="form-control" ng-model="smtpUserName" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-hide="installationDetailsForm" class="form-group">
|
||||
<label class="col-sm-3 control-label">{% trans "Password" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="password" class="form-control" ng-model="smtpPassword" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-hide="installationProgress" class="form-group">
|
||||
<label class="col-sm-3 control-label"></label>
|
||||
<div class="col-sm-4">
|
||||
<button type="button" ng-click="saveSMTPHost()" class="btn btn-primary btn-lg btn-block">{% trans "Save Host" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!---- Create SMTP Host --->
|
||||
|
||||
<!------ List of records --------------->
|
||||
|
||||
<div ng-hide="currentRecords" class="form-group">
|
||||
|
||||
<div class="col-sm-12">
|
||||
|
||||
<table class="table" style="background: var(--bg-primary, white); color: var(--text-primary, #333); border-color: var(--border-color, #ddd);">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "ID" %}</th>
|
||||
<th>{% trans "Owner" %}</th>
|
||||
<th>{% trans "Host" %}</th>
|
||||
<th>{% trans "Port" %}</th>
|
||||
<th>{% trans "Username" %}</th>
|
||||
<th>{% trans "Actions" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="record in records | filter:searchEmails">
|
||||
<td ng-bind="record.id"></td>
|
||||
<td ng-bind="record.owner"></td>
|
||||
<td ng-bind="record.host"></td>
|
||||
<td ng-bind="record.port"></td>
|
||||
<td ng-bind="record.userName"></td>
|
||||
<td >
|
||||
<button type="button" ng-click="smtpHostOperations('verify', record.id)" class="btn ra-100 btn-purple">{% trans "Verify Host" %}</button>
|
||||
<button type="button" ng-click="smtpHostOperations('delete', record.id)" class="btn ra-100 btn-purple">{% trans "Delete" %}</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-4 col-sm-offset-8">
|
||||
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination">
|
||||
<li ng-click="fetchEmails(page)" ng-repeat="page in pagination"><a href="">{$ page $}</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!------ List of records --------------->
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,204 +0,0 @@
|
||||
{% extends "baseTemplate/index.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Send Emails - CyberPanel" %}{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
{% load static %}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
<!-- Current language: {{ LANGUAGE_CODE }} -->
|
||||
|
||||
|
||||
<div class="container">
|
||||
<div id="page-title">
|
||||
<h2>{% trans "Send Emails" %}</h2>
|
||||
<p>{% trans "On this page you can send emails to the lists you created using SMTP Hosts." %}</p>
|
||||
</div>
|
||||
<div ng-controller="sendEmailsCTRL" class="panel" style="background: var(--bg-primary, white); border-color: var(--border-color, #ddd);">
|
||||
<div class="panel-body" style="background: var(--bg-primary, white); color: var(--text-primary, #333);">
|
||||
<h3 class="title-hero">
|
||||
{% trans "Send Emails" %} <img ng-hide="cyberPanelLoading" src="{% static 'images/loading.gif' %}">
|
||||
</h3>
|
||||
<div class="example-box-wrapper">
|
||||
|
||||
|
||||
<form action="/" class="form-horizontal bordered-row">
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">{% trans "Select Template" %} </label>
|
||||
<div class="col-sm-6">
|
||||
<select ng-change="templateSelected()" ng-model="selectedTemplate" class="form-control">
|
||||
{% for items in templateNames %}
|
||||
<option>{{ items }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-hide="availableFunctions" class="form-group">
|
||||
<label class="col-sm-2 control-label"></label>
|
||||
<div class="col-sm-3">
|
||||
<button type="button" ng-disabled="deleteTemplateBTN" data-toggle="modal" data-target="#deleteTemplate" class="btn ra-100 btn-danger">{% trans 'Delete This Template' %}</button>
|
||||
<!--- Delete Template --->
|
||||
<div class="modal fade" id="deleteTemplate" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content" style="background: var(--bg-primary, white); color: var(--text-primary, #333); border-color: var(--border-color, #ddd);">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">{% trans "You are doing to delete this template.." %} <img ng-hide="cyberPanelLoading" src="{% static 'images/loading.gif' %}"></h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{% trans 'Are you sure?' %}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{% trans 'Close' %}</button>
|
||||
<button data-dismiss="modal" ng-click="deleteTemplate()" type="button" class="btn btn-primary">{% trans 'Confirm' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--- Delete Template --->
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<a target="_blank" href="{$ previewLink $}"><button type="button" class="btn ra-100 btn-blue-alt">{% trans 'Preview Template' %}</button></a>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<button ng-disabled="sendEmailBTN" type="button" ng-click="sendEmails()" class="btn ra-100 btn-blue-alt">{% trans 'Send Emails' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-hide="sendEmailsView" class="form-group">
|
||||
<label class="col-sm-3 control-label">{% trans "Select List" %} </label>
|
||||
<div class="col-sm-6">
|
||||
<select ng-model="listName" class="form-control">
|
||||
{% for items in listNames %}
|
||||
<option>{{ items }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-hide="sendEmailsView" class="form-group">
|
||||
<label class="col-sm-3 control-label">{% trans "Select STMP Host" %} </label>
|
||||
<div class="col-sm-6">
|
||||
<select ng-model="host" class="form-control">
|
||||
{% for items in hostNames %}
|
||||
<option>{{ items }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-hide="sendEmailsView" class="form-group">
|
||||
<label class="col-sm-3 control-label">{% trans "" %}</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input ng-model="verificationCheck" type="checkbox" value="">
|
||||
{% trans 'Send to un-verified email addresses.' %}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<label class="col-sm-3 control-label"></label>
|
||||
<div class="col-sm-9">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input ng-model="unsubscribeCheck" type="checkbox" value="">
|
||||
{% trans 'Include unsubscribe link.' %}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-hide="sendEmailsView" class="form-group">
|
||||
<label class="col-sm-3 control-label"></label>
|
||||
<div class="col-sm-4">
|
||||
<button type="button" ng-click="startEmailJob()" class="btn btn-primary btn-lg btn-block">{% trans "Start Job" %}</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!---- Email Job Status --->
|
||||
|
||||
<div ng-hide="jobStatus" class="form-group">
|
||||
<label class="col-sm-2 control-label"></label>
|
||||
<div class="col-sm-7">
|
||||
<div class="alert alert-success text-center" style="background: var(--bg-secondary, #f8f9ff); color: var(--text-primary, #333); border-color: var(--border-color, #ddd);">
|
||||
<h2>{$ currentStatus $}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-hide="jobStatus" class="form-group">
|
||||
<label class="col-sm-3 control-label"></label>
|
||||
<div class="col-sm-4">
|
||||
<button type="button" ng-disabled="goBackDisable" ng-click="goBack()" class="btn btn-primary btn-lg btn-block">{% trans "Go Back" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!---- Email Job Status --->
|
||||
|
||||
|
||||
<!------ List of records --------------->
|
||||
|
||||
<div ng-hide="sendEmailsView" class="form-group">
|
||||
|
||||
<div class="col-sm-12">
|
||||
|
||||
<table class="table" style="background: var(--bg-primary, white); color: var(--text-primary, #333); border-color: var(--border-color, #ddd);">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Job ID" %}</th>
|
||||
<th>{% trans "Date" %}</th>
|
||||
<th>{% trans "SMTP Host" %}</th>
|
||||
<th>{% trans "Total Emails" %}</th>
|
||||
<th>{% trans "Sent" %}</th>
|
||||
<th>{% trans "Failed" %}</th>
|
||||
<th>{% trans "Actions" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="record in records | filter:searchEmails">
|
||||
<td ng-bind="record.id"></td>
|
||||
<td ng-bind="record.date"></td>
|
||||
<td ng-bind="record.host"></td>
|
||||
<td ng-bind="record.totalEmails"></td>
|
||||
<td ng-bind="record.sent"></td>
|
||||
<td ng-bind="record.failed"></td>
|
||||
<td >
|
||||
<button type="button" ng-click="deleteJob(record.id)" class="btn ra-100 btn-purple">{% trans "Delete" %}</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-4 col-sm-offset-8">
|
||||
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination">
|
||||
<li ng-click="fetchEmails(page)" ng-repeat="page in pagination"><a href="">{$ page $}</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!------ List of records --------------->
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -1,31 +0,0 @@
|
||||
from django.urls import path, re_path
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.emailMarketing, name='emailMarketing'),
|
||||
path('fetchUsers', views.fetchUsers, name='fetchUsers'),
|
||||
path('enableDisableMarketing', views.enableDisableMarketing, name='enableDisableMarketing'),
|
||||
path('saveConfigureVerify', views.saveConfigureVerify, name='saveConfigureVerify'),
|
||||
path('fetchVerifyLogs', views.fetchVerifyLogs, name='fetchVerifyLogs'),
|
||||
re_path(r'^(?P<domain>.+)/emailLists$', views.createEmailList, name='createEmailList'),
|
||||
path('submitEmailList', views.submitEmailList, name='submitEmailList'),
|
||||
re_path(r'^(?P<domain>.+)/manageLists$', views.manageLists, name='manageLists'),
|
||||
re_path(r'^(?P<domain>.+)/manageSMTP$', views.manageSMTP, name='manageSMTP'),
|
||||
re_path(r'^(?P<domain>.+)/configureVerify$', views.configureVerify, name='configureVerify'),
|
||||
path('fetchEmails', views.fetchEmails, name='fetchEmails'),
|
||||
path('deleteList', views.deleteList, name='deleteList'),
|
||||
path('emailVerificationJob', views.emailVerificationJob, name='emailVerificationJob'),
|
||||
path('deleteEmail', views.deleteEmail, name='deleteEmail'),
|
||||
path('saveSMTPHost', views.saveSMTPHost, name='saveSMTPHost'),
|
||||
path('fetchSMTPHosts', views.fetchSMTPHosts, name='fetchSMTPHosts'),
|
||||
path('smtpHostOperations', views.smtpHostOperations, name='smtpHostOperations'),
|
||||
path('composeEmailMessage', views.composeEmailMessage, name='composeEmailMessage'),
|
||||
path('saveEmailTemplate', views.saveEmailTemplate, name='saveEmailTemplate'),
|
||||
path('sendEmails', views.sendEmails, name='sendEmails'),
|
||||
re_path(r'^preview/(?P<templateName>[-\w]+)/$', views.templatePreview, name='templatePreview'),
|
||||
path('fetchJobs', views.fetchJobs, name='fetchJobs'),
|
||||
path('startEmailJob', views.startEmailJob, name='startEmailJob'),
|
||||
path('deleteTemplate', views.deleteTemplate, name='deleteTemplate'),
|
||||
path('deleteJob', views.deleteJob, name='deleteJob'),
|
||||
re_path(r'^remove/(?P<listName>[-\w]+)/(?P<emailAddress>\w+@.+)$', views.remove, name='remove'),
|
||||
]
|
||||
@@ -1,215 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.shortcuts import redirect
|
||||
from loginSystem.views import loadLoginPage
|
||||
from .emailMarketingManager import EmailMarketingManager
|
||||
# Create your views here.
|
||||
|
||||
|
||||
def emailMarketing(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
emm = EmailMarketingManager(request)
|
||||
return emm.emailMarketing()
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
def fetchUsers(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
emm = EmailMarketingManager(request)
|
||||
return emm.fetchUsers()
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
def enableDisableMarketing(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
emm = EmailMarketingManager(request)
|
||||
return emm.enableDisableMarketing()
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
def createEmailList(request, domain):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
emm = EmailMarketingManager(request, domain)
|
||||
return emm.createEmailList()
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
def submitEmailList(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
emm = EmailMarketingManager(request)
|
||||
return emm.submitEmailList()
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
def manageLists(request, domain):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
emm = EmailMarketingManager(request, domain)
|
||||
return emm.manageLists()
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
def configureVerify(request, domain):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
emm = EmailMarketingManager(request, domain)
|
||||
return emm.configureVerify()
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
def saveConfigureVerify(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
emm = EmailMarketingManager(request)
|
||||
return emm.saveConfigureVerify()
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
def fetchVerifyLogs(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
emm = EmailMarketingManager(request)
|
||||
return emm.fetchVerifyLogs()
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
def fetchEmails(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
emm = EmailMarketingManager(request)
|
||||
return emm.fetchEmails()
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
def deleteList(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
emm = EmailMarketingManager(request)
|
||||
return emm.deleteList()
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
def emailVerificationJob(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
emm = EmailMarketingManager(request)
|
||||
return emm.emailVerificationJob()
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
def deleteEmail(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
emm = EmailMarketingManager(request)
|
||||
return emm.deleteEmail()
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
def manageSMTP(request, domain):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
emm = EmailMarketingManager(request, domain)
|
||||
return emm.manageSMTP()
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
def saveSMTPHost(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
emm = EmailMarketingManager(request)
|
||||
return emm.saveSMTPHost()
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
def fetchSMTPHosts(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
emm = EmailMarketingManager(request)
|
||||
return emm.fetchSMTPHosts()
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
def smtpHostOperations(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
emm = EmailMarketingManager(request)
|
||||
return emm.smtpHostOperations()
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
def composeEmailMessage(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
emm = EmailMarketingManager(request)
|
||||
return emm.composeEmailMessage()
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
def saveEmailTemplate(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
emm = EmailMarketingManager(request)
|
||||
return emm.saveEmailTemplate()
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
def sendEmails(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
emm = EmailMarketingManager(request)
|
||||
return emm.sendEmails()
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
def templatePreview(request, templateName):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
emm = EmailMarketingManager(request, templateName)
|
||||
return emm.templatePreview()
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
def fetchJobs(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
emm = EmailMarketingManager(request)
|
||||
return emm.fetchJobs()
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
def startEmailJob(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
emm = EmailMarketingManager(request)
|
||||
return emm.startEmailJob()
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
def deleteTemplate(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
emm = EmailMarketingManager(request)
|
||||
return emm.deleteTemplate()
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
def deleteJob(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
emm = EmailMarketingManager(request)
|
||||
return emm.deleteJob()
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
|
||||
def remove(request, listName, emailAddress):
|
||||
try:
|
||||
emm = EmailMarketingManager(request)
|
||||
return emm.remove(listName, emailAddress)
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
@@ -1 +0,0 @@
|
||||
default_app_config = 'examplePlugin.apps.ExamplepluginConfig'
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
@@ -1,8 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ExamplepluginConfig(AppConfig):
|
||||
name = 'examplePlugin'
|
||||
|
||||
def ready(self):
|
||||
from . import signals
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<cyberpanelPluginConfig>
|
||||
<name>examplePlugin</name>
|
||||
<type>plugin</type>
|
||||
<description>This is an example plugin</description>
|
||||
<version>1.0.0</version>
|
||||
<author>usmannasir</author>
|
||||
</cyberpanelPluginConfig>
|
||||
@@ -1,9 +0,0 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class ExamplePlugin(models.Model):
|
||||
name = models.CharField(unique=True, max_length=255)
|
||||
|
||||
class Meta:
|
||||
# db_table = "ExamplePlugin"
|
||||
pass
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/local/CyberCP/bin/python
|
||||
RESET = '\033[0;0m'
|
||||
BLUE = "\033[0;34m"
|
||||
print(BLUE + "Running Post-Install Script..." + RESET)
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/local/CyberCP/bin/python
|
||||
RESET = '\033[0;0m'
|
||||
GREEN = '\033[0;32m'
|
||||
print(GREEN + "Running Pre-Install Script..." + RESET)
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/local/CyberCP/bin/python
|
||||
RESET = '\033[0;0m'
|
||||
GREEN = '\033[0;32m'
|
||||
print(GREEN + "Running Pre-Remove Script..." + RESET)
|
||||
@@ -1,17 +0,0 @@
|
||||
from django.dispatch import receiver
|
||||
from django.http import HttpResponse
|
||||
from websiteFunctions.signals import postWebsiteDeletion
|
||||
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
|
||||
|
||||
|
||||
# This plugin respond to an event after CyberPanel core finished deleting a website.
|
||||
# Original request object is passed, body can be accessed with request.body.
|
||||
|
||||
# If any Event handler returns a response object, CyberPanel will stop further processing and returns your response to browser.
|
||||
# To continue processing just return 200 from your events handlers.
|
||||
|
||||
@receiver(postWebsiteDeletion)
|
||||
def rcvr(sender, **kwargs):
|
||||
request = kwargs['request']
|
||||
logging.writeToFile('Hello World from Example Plugin.')
|
||||
return HttpResponse('Hello World from Example Plugin.')
|
||||
@@ -1,3 +0,0 @@
|
||||
$(document).ready(function () {
|
||||
console.log("using JS in static file...!");
|
||||
});
|
||||
@@ -1,52 +0,0 @@
|
||||
{% extends "baseTemplate/index.html" %}
|
||||
{% load i18n %}
|
||||
{% block styles %}
|
||||
<style>
|
||||
.exampleBody {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block title %}Example plugin - CyberPanel{% endblock %}
|
||||
{% block content %}
|
||||
{% load static %}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
<!-- Current language: {{ LANGUAGE_CODE }} -->
|
||||
<div class="container" id="examplePluginApp">
|
||||
|
||||
<div id="page-title">
|
||||
<h2 id="domainNamePage">{% trans "Example Plugin Page" %}</h2>
|
||||
<p>{% trans "Example Plugin Info" %}</p>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-heading container-fluid">
|
||||
<div class="col-xs-4"><h3 class="panel-title">{% trans "examplePlugin" %}</h3></div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="example-box-wrapper">
|
||||
<p class="exampleBody">[[ pluginBody ]]</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block footer_scripts %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
|
||||
{# <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>#}
|
||||
<script src="{% static 'examplePlugin/examplePlugin.js' %}"></script>
|
||||
<script>
|
||||
let examplePluginApp = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#examplePluginApp',
|
||||
data: function () {
|
||||
return {
|
||||
pluginBody: "Example Plugin Body leveraging templated imported Vue.js",
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -1,7 +0,0 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.examplePlugin, name='examplePlugin'),
|
||||
]
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
from django.shortcuts import render, HttpResponse
|
||||
|
||||
|
||||
# Create your views here.
|
||||
|
||||
def examplePlugin(request):
|
||||
return render(request, 'examplePlugin/examplePlugin.html')
|
||||
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
import base64
|
||||
|
||||
from django.shortcuts import HttpResponse
|
||||
import json
|
||||
@@ -160,12 +161,67 @@ class FileManager:
|
||||
def returnPathEnclosed(self, path):
|
||||
return "'" + path + "'"
|
||||
|
||||
def _moveViaPythonBase64(self, src_path, dest_path, user):
|
||||
"""Fallback: use helper script or Python to move when mv fails (handles special chars)."""
|
||||
try:
|
||||
import subprocess
|
||||
s_b64 = base64.b64encode(src_path.encode('utf-8')).decode('ascii')
|
||||
d_b64 = base64.b64encode(dest_path.encode('utf-8')).decode('ascii')
|
||||
helper = '/usr/local/CyberCP/bin/safe-move-path'
|
||||
if os.path.isfile(helper) and os.access(helper, os.X_OK):
|
||||
cmd = [helper, s_b64, d_b64]
|
||||
if os.getuid() != 0:
|
||||
cmd = ['sudo', '-n'] + cmd
|
||||
try:
|
||||
res = subprocess.run(cmd, capture_output=True, timeout=30)
|
||||
if res.returncode == 0:
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.writeToFile(f"_moveViaPythonBase64 sudo helper failed: {str(e)}")
|
||||
command = '%s %s %s' % (helper, s_b64, d_b64)
|
||||
else:
|
||||
code = "import shutil,base64,sys; s=base64.b64decode(sys.argv[1]).decode(); d=base64.b64decode(sys.argv[2]).decode(); shutil.move(s,d)"
|
||||
command = "/usr/bin/python3 -c '%s' %s %s" % (code, s_b64, d_b64)
|
||||
result = ProcessUtilities.executioner(command, user)
|
||||
return result == 1
|
||||
except Exception as e:
|
||||
logging.writeToFile(f"_moveViaPythonBase64 failed: {str(e)}")
|
||||
return False
|
||||
|
||||
def _deleteViaPythonBase64(self, path, user):
|
||||
"""Fallback: use helper script or Python to delete when rm fails (handles special chars)."""
|
||||
try:
|
||||
import subprocess
|
||||
p_b64 = base64.b64encode(path.encode('utf-8')).decode('ascii')
|
||||
helper = '/usr/local/CyberCP/bin/safe-delete-path'
|
||||
if os.path.isfile(helper) and os.access(helper, os.X_OK):
|
||||
cmd = [helper, p_b64]
|
||||
if os.getuid() != 0:
|
||||
cmd = ['sudo', '-n'] + cmd
|
||||
try:
|
||||
res = subprocess.run(cmd, capture_output=True, timeout=30)
|
||||
if res.returncode == 0:
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.writeToFile(f"_deleteViaPythonBase64 sudo helper failed: {str(e)}")
|
||||
if os.path.isfile(helper) and os.access(helper, os.X_OK):
|
||||
command = '%s %s' % (helper, p_b64)
|
||||
else:
|
||||
code = "import os,base64,sys,shutil; p=base64.b64decode(sys.argv[1]).decode(); (os.path.isfile(p) and os.remove(p)) or (os.path.isdir(p) and shutil.rmtree(p))"
|
||||
command = "/usr/bin/python3 -c '%s' %s" % (code, p_b64)
|
||||
result = ProcessUtilities.executioner(command, user)
|
||||
return result == 1
|
||||
except Exception as e:
|
||||
logging.writeToFile(f"_deleteViaPythonBase64 failed: {str(e)}")
|
||||
return False
|
||||
|
||||
def changeOwner(self, path):
|
||||
try:
|
||||
domainName = self.data['domainName']
|
||||
website = Websites.objects.get(domain=domainName)
|
||||
homePath = '/home/%s' % (domainName)
|
||||
|
||||
if path.find('..') > -1:
|
||||
if not ACLManager.isPathInsideHome(path, homePath):
|
||||
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
|
||||
|
||||
command = "chown -R " + website.externalApp + ':' + website.externalApp + ' ' + self.returnPathEnclosed(path)
|
||||
@@ -186,8 +242,7 @@ class FileManager:
|
||||
|
||||
pathCheck = '/home/%s' % (domainName)
|
||||
|
||||
if self.data['completeStartingPath'].find(pathCheck) == -1 or self.data['completeStartingPath'].find(
|
||||
'..') > -1:
|
||||
if not ACLManager.isPathInsideHome(self.data['completeStartingPath'], pathCheck):
|
||||
return self.ajaxPre(0, 'Not allowed to browse this path, going back home!')
|
||||
|
||||
command = "ls -la --group-directories-first " + self.returnPathEnclosed(
|
||||
@@ -197,8 +252,7 @@ class FileManager:
|
||||
except:
|
||||
pathCheck = '/'
|
||||
|
||||
if self.data['completeStartingPath'].find(pathCheck) == -1 or self.data['completeStartingPath'].find(
|
||||
'..') > -1:
|
||||
if not ACLManager.isPathInsideHome(self.data['completeStartingPath'], pathCheck):
|
||||
return self.ajaxPre(0, 'Not allowed to browse this path, going back home!')
|
||||
|
||||
command = "ls -la --group-directories-first " + self.returnPathEnclosed(
|
||||
@@ -314,7 +368,7 @@ class FileManager:
|
||||
website = Websites.objects.get(domain=domainName)
|
||||
homePath = '/home/%s' % (domainName)
|
||||
|
||||
if self.data['fileName'].find('..') > -1 or self.data['fileName'].find(homePath) == -1:
|
||||
if not ACLManager.isPathInsideHome(self.data['fileName'], homePath):
|
||||
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
|
||||
|
||||
command = "touch " + self.returnPathEnclosed(self.data['fileName'])
|
||||
@@ -327,7 +381,7 @@ class FileManager:
|
||||
except:
|
||||
homePath = '/'
|
||||
|
||||
if self.data['fileName'].find('..') > -1 or self.data['fileName'].find(homePath) == -1:
|
||||
if not ACLManager.isPathInsideHome(self.data['fileName'], homePath):
|
||||
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
|
||||
|
||||
command = "touch " + self.returnPathEnclosed(self.data['fileName'])
|
||||
@@ -349,7 +403,7 @@ class FileManager:
|
||||
|
||||
homePath = '/home/%s' % (domainName)
|
||||
|
||||
if self.data['folderName'].find('..') > -1 or self.data['folderName'].find(homePath) == -1:
|
||||
if not ACLManager.isPathInsideHome(self.data['folderName'], homePath):
|
||||
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
|
||||
|
||||
command = "mkdir " + self.returnPathEnclosed(self.data['folderName'])
|
||||
@@ -363,7 +417,7 @@ class FileManager:
|
||||
except:
|
||||
homePath = '/'
|
||||
|
||||
if self.data['folderName'].find('..') > -1 or self.data['folderName'].find(homePath) == -1:
|
||||
if not ACLManager.isPathInsideHome(self.data['folderName'], homePath):
|
||||
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
|
||||
|
||||
command = "mkdir " + self.returnPathEnclosed(self.data['folderName'])
|
||||
@@ -392,25 +446,27 @@ class FileManager:
|
||||
website = Websites.objects.get(domain=domainName)
|
||||
self.homePath = '/home/%s' % (domainName)
|
||||
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Attempting to delete files/folders for domain: {domainName}")
|
||||
logging.writeToFile(f"Attempting to delete files/folders for domain: {domainName}")
|
||||
|
||||
RemoveOK = 1
|
||||
|
||||
# Test if directory is writable
|
||||
command = 'touch %s/public_html/hello.txt' % (self.homePath)
|
||||
result = ProcessUtilities.outputExecutioner(command)
|
||||
if result is None:
|
||||
result = ''
|
||||
|
||||
if result.find('cannot touch') > -1:
|
||||
if isinstance(result, (str, bytes)) and ('cannot touch' in str(result) or 'Permission denied' in str(result)):
|
||||
RemoveOK = 0
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Directory {self.homePath} is not writable, removing chattr flags")
|
||||
logging.writeToFile(f"Directory {self.homePath} is not writable, removing chattr flags")
|
||||
|
||||
# Remove immutable flag from entire directory
|
||||
# Remove immutable flag from entire directory (executioner returns 1=success, 0=failure)
|
||||
command = 'chattr -R -i %s' % (self.homePath)
|
||||
result = ProcessUtilities.executioner(command)
|
||||
if result.find('cannot') > -1:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Warning: Failed to remove chattr -i from {self.homePath}: {result}")
|
||||
if result != 1:
|
||||
logging.writeToFile(f"Warning: Failed to remove chattr -i from {self.homePath}: {result}")
|
||||
else:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Successfully removed chattr -i from {self.homePath}")
|
||||
logging.writeToFile(f"Successfully removed chattr -i from {self.homePath}")
|
||||
|
||||
else:
|
||||
command = 'rm -f %s/public_html/hello.txt' % (self.homePath)
|
||||
@@ -421,24 +477,22 @@ class FileManager:
|
||||
itemPath = self.data['path'] + '/' + item
|
||||
|
||||
# Security check - prevent path traversal
|
||||
if itemPath.find('..') > -1 or itemPath.find(self.homePath) == -1:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Security violation: Attempted to delete outside home directory: {itemPath}")
|
||||
if not ACLManager.isPathInsideHome(itemPath, self.homePath):
|
||||
logging.writeToFile(f"Security violation: Attempted to delete outside home directory: {itemPath}")
|
||||
return self.ajaxPre(0, 'Not allowed to delete files outside home directory!')
|
||||
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Deleting: {itemPath}")
|
||||
logging.writeToFile(f"Deleting: {itemPath}")
|
||||
|
||||
if skipTrash:
|
||||
# Permanent deletion
|
||||
# Permanent deletion (executioner returns 1=success, 0=failure)
|
||||
command = 'rm -rf ' + self.returnPathEnclosed(itemPath)
|
||||
result = ProcessUtilities.executioner(command, website.externalApp)
|
||||
if result.find('cannot') > -1 or result.find('Permission denied') > -1:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Failed to delete {itemPath}: {result}")
|
||||
# Try with sudo if available
|
||||
command = 'sudo rm -rf ' + self.returnPathEnclosed(itemPath)
|
||||
result = ProcessUtilities.executioner(command, website.externalApp)
|
||||
if result.find('cannot') > -1 or result.find('Permission denied') > -1:
|
||||
return self.ajaxPre(0, f'Failed to delete {item}: {result}')
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Successfully deleted: {itemPath}")
|
||||
if result != 1:
|
||||
logging.writeToFile(f"Failed to delete {itemPath}: result={result}, trying Python fallback")
|
||||
# Fallback: Python+base64 to handle special chars in paths
|
||||
if not self._deleteViaPythonBase64(itemPath, website.externalApp):
|
||||
return self.ajaxPre(0, f'Failed to delete {item}')
|
||||
logging.writeToFile(f"Successfully deleted: {itemPath}")
|
||||
|
||||
## Update disk usage in background
|
||||
command = "/usr/local/CyberCP/bin/python /usr/local/CyberCP/plogical/IncScheduler.py UpdateDiskUsageForceDomain --domainName %s" % (domainName)
|
||||
@@ -447,46 +501,44 @@ class FileManager:
|
||||
# Move to trash
|
||||
trashPath = '%s/.trash' % (self.homePath)
|
||||
|
||||
# Ensure trash directory exists
|
||||
# Ensure trash directory exists (executioner returns 1=success, 0=failure)
|
||||
command = 'mkdir -p %s' % (trashPath)
|
||||
result = ProcessUtilities.executioner(command, website.externalApp)
|
||||
if result.find('cannot') > -1:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Failed to create trash directory: {result}")
|
||||
return self.ajaxPre(0, f'Failed to create trash directory: {result}')
|
||||
if result != 1:
|
||||
logging.writeToFile(f"Failed to create trash directory: result={result}")
|
||||
return self.ajaxPre(0, f'Failed to create trash directory')
|
||||
|
||||
# Save to trash database
|
||||
try:
|
||||
Trash(website=website, originalPath=self.returnPathEnclosed(self.data['path']),
|
||||
fileName=self.returnPathEnclosed(item)).save()
|
||||
except Exception as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Failed to save trash record: {str(e)}")
|
||||
logging.writeToFile(f"Failed to save trash record: {str(e)}")
|
||||
|
||||
# Move to trash
|
||||
# Move to trash (executioner returns 1=success, 0=failure)
|
||||
command = 'mv %s %s' % (self.returnPathEnclosed(itemPath), trashPath)
|
||||
result = ProcessUtilities.executioner(command, website.externalApp)
|
||||
if result.find('cannot') > -1 or result.find('Permission denied') > -1:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Failed to move to trash {itemPath}: {result}")
|
||||
# Try with sudo if available
|
||||
command = 'sudo mv %s %s' % (self.returnPathEnclosed(itemPath), trashPath)
|
||||
result = ProcessUtilities.executioner(command, website.externalApp)
|
||||
if result.find('cannot') > -1 or result.find('Permission denied') > -1:
|
||||
return self.ajaxPre(0, f'Failed to move {item} to trash: {result}')
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Successfully moved to trash: {itemPath}")
|
||||
if result != 1:
|
||||
logging.writeToFile(f"Failed to move to trash {itemPath}: result={result}, trying Python fallback")
|
||||
# Fallback: Python+base64 to handle special chars in paths (e.g. lscpd/sendCommand)
|
||||
if not self._moveViaPythonBase64(itemPath, trashPath, website.externalApp):
|
||||
return self.ajaxPre(0, f'Failed to move {item} to trash')
|
||||
logging.writeToFile(f"Successfully moved to trash: {itemPath}")
|
||||
|
||||
## Update disk usage in background
|
||||
command = "/usr/local/CyberCP/bin/python /usr/local/CyberCP/plogical/IncScheduler.py UpdateDiskUsageForceDomain --domainName %s" % (domainName)
|
||||
ProcessUtilities.popenExecutioner(command)
|
||||
|
||||
if RemoveOK == 0:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Restoring chattr +i flags for {self.homePath}")
|
||||
logging.writeToFile(f"Restoring chattr +i flags for {self.homePath}")
|
||||
|
||||
# Restore immutable flag to entire directory
|
||||
# Restore immutable flag to entire directory (executioner returns 1=success, 0=failure)
|
||||
command = 'chattr -R +i %s' % (self.homePath)
|
||||
result = ProcessUtilities.executioner(command)
|
||||
if result.find('cannot') > -1:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Warning: Failed to restore chattr +i to {self.homePath}: {result}")
|
||||
if result != 1:
|
||||
logging.writeToFile(f"Warning: Failed to restore chattr +i to {self.homePath}: result={result}")
|
||||
else:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Successfully restored chattr +i to {self.homePath}")
|
||||
logging.writeToFile(f"Successfully restored chattr +i to {self.homePath}")
|
||||
|
||||
# Allow specific directories to remain mutable
|
||||
mutable_dirs = ['/logs/', '/.trash/', '/backup/', '/incbackup/', '/lscache/', '/.cagefs/']
|
||||
@@ -494,68 +546,74 @@ class FileManager:
|
||||
dir_path = self.homePath + dir_name
|
||||
command = 'chattr -R -i %s' % (dir_path)
|
||||
result = ProcessUtilities.executioner(command)
|
||||
if result.find('cannot') > -1:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Warning: Failed to remove chattr +i from {dir_path}: {result}")
|
||||
if result != 1:
|
||||
logging.writeToFile(f"Warning: Failed to remove chattr +i from {dir_path}: result={result}")
|
||||
else:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Successfully removed chattr +i from {dir_path}")
|
||||
logging.writeToFile(f"Successfully removed chattr +i from {dir_path}")
|
||||
except Exception as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Error in deleteFolderOrFile for {domainName}: {str(e)}")
|
||||
import traceback
|
||||
logging.writeToFile(f"Error in deleteFolderOrFile for {domainName}: {str(e)}")
|
||||
logging.writeToFile(traceback.format_exc())
|
||||
try:
|
||||
skipTrash = self.data['skipTrash']
|
||||
except:
|
||||
skipTrash = False
|
||||
|
||||
|
||||
# Fallback to root path for system files
|
||||
# Fallback to root path for system files (Root File Manager, domainName empty)
|
||||
self.homePath = '/'
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Using fallback deletion for system files in {self.data['path']}")
|
||||
logging.writeToFile(f"Using fallback deletion for system files in {self.data['path']}")
|
||||
|
||||
RemoveOK = 1
|
||||
|
||||
# Test if directory is writable
|
||||
command = 'touch %s/public_html/hello.txt' % (self.homePath)
|
||||
# Test if we can write (use /tmp for root path since /public_html doesn't exist at /)
|
||||
test_path = '/tmp' if self.homePath == '/' else (self.homePath + '/public_html')
|
||||
command = 'touch %s/hello.txt' % (test_path)
|
||||
result = ProcessUtilities.outputExecutioner(command)
|
||||
if result is None:
|
||||
result = ''
|
||||
|
||||
if result.find('cannot touch') > -1:
|
||||
if isinstance(result, (str, bytes)) and ('cannot touch' in str(result) or 'Permission denied' in str(result)):
|
||||
RemoveOK = 0
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Directory {self.homePath} is not writable, removing chattr flags")
|
||||
logging.writeToFile(f"Directory {self.homePath} is not writable, removing chattr flags")
|
||||
|
||||
command = 'chattr -R -i %s' % (self.homePath)
|
||||
result = ProcessUtilities.executioner(command)
|
||||
if result.find('cannot') > -1:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Warning: Failed to remove chattr -i from {self.homePath}: {result}")
|
||||
if result != 1:
|
||||
logging.writeToFile(f"Warning: Failed to remove chattr -i from {self.homePath}: {result}")
|
||||
|
||||
else:
|
||||
command = 'rm -f %s/public_html/hello.txt' % (self.homePath)
|
||||
command = 'rm -f %s/hello.txt' % (test_path)
|
||||
ProcessUtilities.executioner(command)
|
||||
|
||||
for item in self.data['fileAndFolders']:
|
||||
itemPath = self.data['path'] + '/' + item
|
||||
base = self.data['path'].rstrip('/') or '/'
|
||||
itemPath = base + '/' + item
|
||||
|
||||
# Security check for system files
|
||||
if itemPath.find('..') > -1 or itemPath.find(self.homePath) == -1:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Security violation: Attempted to delete outside allowed path: {itemPath}")
|
||||
if not ACLManager.isPathInsideHome(itemPath, self.homePath):
|
||||
logging.writeToFile(f"Security violation: Attempted to delete outside allowed path: {itemPath}")
|
||||
return self.ajaxPre(0, 'Not allowed to delete files outside allowed path!')
|
||||
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Deleting system file: {itemPath}")
|
||||
logging.writeToFile(f"Deleting system file: {itemPath}")
|
||||
|
||||
if skipTrash:
|
||||
command = 'rm -rf ' + self.returnPathEnclosed(itemPath)
|
||||
result = ProcessUtilities.executioner(command)
|
||||
if result.find('cannot') > -1 or result.find('Permission denied') > -1:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Failed to delete system file {itemPath}: {result}")
|
||||
return self.ajaxPre(0, f'Failed to delete {item}: {result}')
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Successfully deleted system file: {itemPath}")
|
||||
if result != 1:
|
||||
logging.writeToFile(f"Failed to delete system file {itemPath}: result={result}, trying Python fallback")
|
||||
if not self._deleteViaPythonBase64(itemPath, None):
|
||||
return self.ajaxPre(0, f'Failed to delete {item}')
|
||||
logging.writeToFile(f"Successfully deleted system file: {itemPath}")
|
||||
|
||||
|
||||
if RemoveOK == 0:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Restoring chattr +i flags for system path: {self.homePath}")
|
||||
logging.writeToFile(f"Restoring chattr +i flags for system path: {self.homePath}")
|
||||
command = 'chattr -R +i %s' % (self.homePath)
|
||||
result = ProcessUtilities.executioner(command)
|
||||
if result.find('cannot') > -1:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Warning: Failed to restore chattr +i to system path {self.homePath}: {result}")
|
||||
if result != 1:
|
||||
logging.writeToFile(f"Warning: Failed to restore chattr +i to system path {self.homePath}: result={result}")
|
||||
else:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Successfully restored chattr +i to system path {self.homePath}")
|
||||
logging.writeToFile(f"Successfully restored chattr +i to system path {self.homePath}")
|
||||
|
||||
# Allow specific directories to remain mutable for system files
|
||||
mutable_dirs = ['/logs/', '/.trash/', '/backup/', '/incbackup/', '/lscache/', '/.cagefs/']
|
||||
@@ -563,17 +621,17 @@ class FileManager:
|
||||
dir_path = self.homePath + dir_name
|
||||
command = 'chattr -R -i %s' % (dir_path)
|
||||
result = ProcessUtilities.executioner(command)
|
||||
if result.find('cannot') > -1:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Warning: Failed to remove chattr +i from system {dir_path}: {result}")
|
||||
if result != 1:
|
||||
logging.writeToFile(f"Warning: Failed to remove chattr +i from system {dir_path}: result={result}")
|
||||
else:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Successfully removed chattr +i from system {dir_path}")
|
||||
logging.writeToFile(f"Successfully removed chattr +i from system {dir_path}")
|
||||
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"File deletion completed successfully for domain: {domainName}")
|
||||
logging.writeToFile(f"File deletion completed successfully for domain: {domainName}")
|
||||
json_data = json.dumps(finalData)
|
||||
return HttpResponse(json_data)
|
||||
|
||||
except BaseException as msg:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Critical error in deleteFolderOrFile: {str(msg)}")
|
||||
logging.writeToFile(f"Critical error in deleteFolderOrFile: {str(msg)}")
|
||||
return self.ajaxPre(0, f"File deletion failed: {str(msg)}")
|
||||
|
||||
def restore(self):
|
||||
@@ -593,8 +651,7 @@ class FileManager:
|
||||
|
||||
for item in self.data['fileAndFolders']:
|
||||
|
||||
if (self.data['path'] + '/' + item).find('..') > -1 or (self.data['path'] + '/' + item).find(
|
||||
self.homePath) == -1:
|
||||
if not ACLManager.isPathInsideHome(self.data['path'] + '/' + item, self.homePath):
|
||||
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
|
||||
|
||||
trashPath = '%s/.trash' % (self.homePath)
|
||||
@@ -628,13 +685,12 @@ class FileManager:
|
||||
|
||||
homePath = '/home/%s' % (domainName)
|
||||
|
||||
if self.data['newPath'].find('..') > -1 or self.data['newPath'].find(homePath) == -1:
|
||||
if not ACLManager.isPathInsideHome(self.data['newPath'], homePath):
|
||||
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
|
||||
|
||||
if len(self.data['fileAndFolders']) == 1:
|
||||
|
||||
if (self.data['basePath'] + '/' + self.data['fileAndFolders'][0]).find('..') > -1 or (
|
||||
self.data['basePath'] + '/' + self.data['fileAndFolders'][0]).find(homePath) == -1:
|
||||
if not ACLManager.isPathInsideHome(self.data['basePath'] + '/' + self.data['fileAndFolders'][0], homePath):
|
||||
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
|
||||
|
||||
command = 'yes| cp -Rf %s %s' % (
|
||||
@@ -654,8 +710,7 @@ class FileManager:
|
||||
ProcessUtilities.executioner(command, website.externalApp)
|
||||
|
||||
for item in self.data['fileAndFolders']:
|
||||
if (self.data['basePath'] + '/' + item).find('..') > -1 or (self.data['basePath'] + '/' + item).find(
|
||||
homePath) == -1:
|
||||
if not ACLManager.isPathInsideHome(self.data['basePath'] + '/' + item, homePath):
|
||||
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
|
||||
|
||||
command = '%scp -Rf ' % ('yes |') + self.returnPathEnclosed(
|
||||
@@ -672,13 +727,12 @@ class FileManager:
|
||||
|
||||
homePath = '/'
|
||||
|
||||
if self.data['newPath'].find('..') > -1 or self.data['newPath'].find(homePath) == -1:
|
||||
if not ACLManager.isPathInsideHome(self.data['newPath'], homePath):
|
||||
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
|
||||
|
||||
if len(self.data['fileAndFolders']) == 1:
|
||||
|
||||
if (self.data['basePath'] + '/' + self.data['fileAndFolders'][0]).find('..') > -1 or (
|
||||
self.data['basePath'] + '/' + self.data['fileAndFolders'][0]).find(homePath) == -1:
|
||||
if not ACLManager.isPathInsideHome(self.data['basePath'] + '/' + self.data['fileAndFolders'][0], homePath):
|
||||
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
|
||||
|
||||
command = 'yes| cp -Rf %s %s' % (
|
||||
@@ -698,9 +752,7 @@ class FileManager:
|
||||
ProcessUtilities.executioner(command)
|
||||
|
||||
for item in self.data['fileAndFolders']:
|
||||
if (self.data['basePath'] + '/' + item).find('..') > -1 or (
|
||||
self.data['basePath'] + '/' + item).find(
|
||||
homePath) == -1:
|
||||
if not ACLManager.isPathInsideHome(self.data['basePath'] + '/' + item, homePath):
|
||||
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
|
||||
|
||||
command = '%scp -Rf ' % ('yes |') + self.returnPathEnclosed(
|
||||
@@ -735,12 +787,10 @@ class FileManager:
|
||||
|
||||
for item in self.data['fileAndFolders']:
|
||||
|
||||
if (self.data['basePath'] + '/' + item).find('..') > -1 or (self.data['basePath'] + '/' + item).find(
|
||||
homePath) == -1:
|
||||
if not ACLManager.isPathInsideHome(self.data['basePath'] + '/' + item, homePath):
|
||||
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
|
||||
|
||||
if (self.data['newPath'] + '/' + item).find('..') > -1 or (self.data['newPath'] + '/' + item).find(
|
||||
homePath) == -1:
|
||||
if not ACLManager.isPathInsideHome(self.data['newPath'] + '/' + item, homePath):
|
||||
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
|
||||
|
||||
command = 'mv ' + self.returnPathEnclosed(
|
||||
@@ -765,13 +815,10 @@ class FileManager:
|
||||
|
||||
for item in self.data['fileAndFolders']:
|
||||
|
||||
if (self.data['basePath'] + '/' + item).find('..') > -1 or (
|
||||
self.data['basePath'] + '/' + item).find(
|
||||
homePath) == -1:
|
||||
if not ACLManager.isPathInsideHome(self.data['basePath'] + '/' + item, homePath):
|
||||
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
|
||||
|
||||
if (self.data['newPath'] + '/' + item).find('..') > -1 or (self.data['newPath'] + '/' + item).find(
|
||||
homePath) == -1:
|
||||
if not ACLManager.isPathInsideHome(self.data['newPath'] + '/' + item, homePath):
|
||||
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
|
||||
|
||||
command = 'mv ' + self.returnPathEnclosed(
|
||||
@@ -803,11 +850,10 @@ class FileManager:
|
||||
|
||||
homePath = '/home/%s' % (domainName)
|
||||
|
||||
if (self.data['basePath'] + '/' + self.data['existingName']).find('..') > -1 or (
|
||||
self.data['basePath'] + '/' + self.data['existingName']).find(homePath) == -1:
|
||||
if not ACLManager.isPathInsideHome(self.data['basePath'] + '/' + self.data['existingName'], homePath):
|
||||
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
|
||||
|
||||
if (self.data['newFileName']).find('..') > -1 or (self.data['basePath']).find(homePath) == -1:
|
||||
if not ACLManager.isPathInsideHome(self.data['basePath'] + '/' + self.data['newFileName'], homePath):
|
||||
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
|
||||
|
||||
command = 'mv ' + self.returnPathEnclosed(
|
||||
@@ -819,11 +865,10 @@ class FileManager:
|
||||
except:
|
||||
homePath = '/'
|
||||
|
||||
if (self.data['basePath'] + '/' + self.data['existingName']).find('..') > -1 or (
|
||||
self.data['basePath'] + '/' + self.data['existingName']).find(homePath) == -1:
|
||||
if not ACLManager.isPathInsideHome(self.data['basePath'] + '/' + self.data['existingName'], homePath):
|
||||
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
|
||||
|
||||
if (self.data['newFileName']).find('..') > -1 or (self.data['basePath']).find(homePath) == -1:
|
||||
if not ACLManager.isPathInsideHome(self.data['basePath'] + '/' + self.data['newFileName'], homePath):
|
||||
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
|
||||
|
||||
command = 'mv ' + self.returnPathEnclosed(
|
||||
@@ -850,7 +895,7 @@ class FileManager:
|
||||
|
||||
pathCheck = '/home/%s' % (domainName)
|
||||
|
||||
if self.data['fileName'].find(pathCheck) == -1 or self.data['fileName'].find('..') > -1:
|
||||
if not ACLManager.isPathInsideHome(self.data['fileName'], pathCheck):
|
||||
return self.ajaxPre(0, 'Not allowed.')
|
||||
|
||||
# Ensure proper UTF-8 handling for file reading
|
||||
@@ -860,7 +905,7 @@ class FileManager:
|
||||
except:
|
||||
pathCheck = '/'
|
||||
|
||||
if self.data['fileName'].find(pathCheck) == -1 or self.data['fileName'].find('..') > -1:
|
||||
if not ACLManager.isPathInsideHome(self.data['fileName'], pathCheck):
|
||||
return self.ajaxPre(0, 'Not allowed.')
|
||||
|
||||
# Ensure proper UTF-8 handling for file reading
|
||||
@@ -958,11 +1003,11 @@ class FileManager:
|
||||
if result.find('->') > -1:
|
||||
return self.ajaxPre(0, "Symlink attack.")
|
||||
|
||||
if ACLManager.commandInjectionCheck(self.data['completePath'] + '/' + myfile.name) == 1:
|
||||
uploadPathFull = self.data['completePath'] + '/' + myfile.name
|
||||
if not ACLManager.isFilePathSafeForShell(uploadPathFull):
|
||||
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
|
||||
|
||||
if (self.data['completePath'] + '/' + myfile.name).find(pathCheck) == -1 or (
|
||||
(self.data['completePath'] + '/' + myfile.name)).find('..') > -1:
|
||||
if not ACLManager.isPathInsideHome(uploadPathFull, pathCheck):
|
||||
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
|
||||
|
||||
command = 'cp ' + self.returnPathEnclosed(
|
||||
@@ -985,11 +1030,11 @@ class FileManager:
|
||||
command = 'ls -la %s' % (self.data['completePath'])
|
||||
result = ProcessUtilities.outputExecutioner(command)
|
||||
logging.writeToFile("upload file res %s" % result)
|
||||
if ACLManager.commandInjectionCheck(self.data['completePath'] + '/' + myfile.name) == 1:
|
||||
uploadPathFull = self.data['completePath'] + '/' + myfile.name
|
||||
if not ACLManager.isFilePathSafeForShell(uploadPathFull):
|
||||
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
|
||||
|
||||
if (self.data['completePath'] + '/' + myfile.name).find(pathCheck) == -1 or (
|
||||
(self.data['completePath'] + '/' + myfile.name)).find('..') > -1:
|
||||
if not ACLManager.isPathInsideHome(uploadPathFull, pathCheck):
|
||||
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
|
||||
|
||||
command = 'cp ' + self.returnPathEnclosed(
|
||||
@@ -1034,10 +1079,10 @@ class FileManager:
|
||||
|
||||
homePath = '/home/%s' % (domainName)
|
||||
|
||||
if self.data['extractionLocation'].find('..') > -1 or self.data['extractionLocation'].find(homePath) == -1:
|
||||
if not ACLManager.isPathInsideHome(self.data['extractionLocation'], homePath):
|
||||
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
|
||||
|
||||
if self.data['fileToExtract'].find('..') > -1 or self.data['fileToExtract'].find(homePath) == -1:
|
||||
if not ACLManager.isPathInsideHome(self.data['fileToExtract'], homePath):
|
||||
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
|
||||
|
||||
if self.data['extractionType'] == 'zip':
|
||||
@@ -1065,11 +1110,10 @@ class FileManager:
|
||||
|
||||
homePath = '/'
|
||||
|
||||
if self.data['extractionLocation'].find('..') > -1 or self.data['extractionLocation'].find(
|
||||
homePath) == -1:
|
||||
if not ACLManager.isPathInsideHome(self.data['extractionLocation'], homePath):
|
||||
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
|
||||
|
||||
if self.data['fileToExtract'].find('..') > -1 or self.data['fileToExtract'].find(homePath) == -1:
|
||||
if not ACLManager.isPathInsideHome(self.data['fileToExtract'], homePath):
|
||||
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
|
||||
|
||||
if self.data['extractionType'] == 'zip':
|
||||
@@ -1130,8 +1174,7 @@ class FileManager:
|
||||
|
||||
for item in self.data['listOfFiles']:
|
||||
|
||||
if (self.data['basePath'] + item).find('..') > -1 or (self.data['basePath'] + item).find(
|
||||
homePath) == -1:
|
||||
if not ACLManager.isPathInsideHome(self.data['basePath'] + item, homePath):
|
||||
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
|
||||
|
||||
command = '%s%s ' % (command, self.returnPathEnclosed(item))
|
||||
@@ -1168,8 +1211,7 @@ class FileManager:
|
||||
|
||||
for item in self.data['listOfFiles']:
|
||||
|
||||
if (self.data['basePath'] + item).find('..') > -1 or (self.data['basePath'] + item).find(
|
||||
homePath) == -1:
|
||||
if not ACLManager.isPathInsideHome(self.data['basePath'] + item, homePath):
|
||||
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
|
||||
command = '%s%s ' % (command, self.returnPathEnclosed(item))
|
||||
|
||||
|
||||
@@ -82,6 +82,15 @@ fileManager.controller('fileManagerCtrl', function ($scope, $http, FileUploader,
|
||||
$scope.showUploadBox = function () {
|
||||
$('#uploadBox').modal('show');
|
||||
};
|
||||
// Fix aria-hidden a11y: move focus out of modal before hide so no focused descendant retains focus
|
||||
$(document).on('hide.bs.modal', '.modal', function () {
|
||||
var modal = this;
|
||||
if (document.activeElement && modal.contains(document.activeElement)) {
|
||||
var trigger = document.getElementById('uploadTriggerBtn');
|
||||
if (trigger && modal.id === 'uploadBox') { trigger.focus(); }
|
||||
else { document.activeElement.blur(); }
|
||||
}
|
||||
});
|
||||
|
||||
$scope.showHTMLEditorModal = function (MainFM= 0) {
|
||||
$scope.htmlEditorLoading = false;
|
||||
@@ -1147,7 +1156,8 @@ fileManager.controller('fileManagerCtrl', function ($scope, $http, FileUploader,
|
||||
});
|
||||
$scope.fetchForTableSecondary(null, 'refresh');
|
||||
} else {
|
||||
var notification = alertify.notify('Files/Folders can not be deleted', 'error', 5, function () {
|
||||
var errMsg = (response.data && response.data.error_message) ? response.data.error_message : 'Files/Folders can not be deleted';
|
||||
var notification = alertify.notify(errMsg, 'error', 8, function () {
|
||||
console.log('dismissed');
|
||||
});
|
||||
}
|
||||
@@ -1155,6 +1165,10 @@ fileManager.controller('fileManagerCtrl', function ($scope, $http, FileUploader,
|
||||
}
|
||||
|
||||
function cantLoadInitialDatas(response) {
|
||||
var err = (response && response.data && (response.data.error_message || response.data.message)) ||
|
||||
(response && response.statusText) || 'Request failed';
|
||||
if (response && response.status === 0) err = 'Network error';
|
||||
alertify.notify(err, 'error', 8);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -156,6 +156,14 @@ function findFileExtension(fileName) {
|
||||
$scope.showUploadBox = function () {
|
||||
$("#uploadBox").modal();
|
||||
};
|
||||
$(document).on("hide.bs.modal", ".modal", function () {
|
||||
var modal = this;
|
||||
if (document.activeElement && modal.contains(document.activeElement)) {
|
||||
var trigger = document.getElementById("uploadTriggerBtn");
|
||||
if (trigger && modal.id === "uploadBox") { trigger.focus(); }
|
||||
else { document.activeElement.blur(); }
|
||||
}
|
||||
});
|
||||
|
||||
$scope.showHTMLEditorModal = function (MainFM = 0) {
|
||||
$scope.fileInEditor = allFilesAndFolders[0];
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<link rel="icon" type="image/png" href="{% static 'baseTemplate/assets/finalBase/favicon.png' %}">
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css"
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-beta.2/css/bootstrap.min.css"
|
||||
integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="{% static 'filemanager/images/fonts/css/font-awesome.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'filemanager/css/fileManager.css' %}">
|
||||
@@ -186,7 +186,7 @@
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js"
|
||||
integrity="sha384-vFJXuSJphROIrBnz7yo7oB41mKfc8JzQZiCq4NCceLEaO4IHwicKwpJf9c9IpFgh"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js"
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-beta.2/js/bootstrap.min.js"
|
||||
integrity="sha384-alpBpkh1PFOepccYVYDB4do5UnbKysX5WZXm3XxPqe5iKTfUKjNkCk9SaVuEZflJ"
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<link rel="icon" type="image/png" href="{% static 'baseTemplate/assets/finalBase/favicon.png' %}">
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-beta.2/css/bootstrap.min.css" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="{% static 'filemanager/images/fonts/css/font-awesome.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'filemanager/css/fileManager.css' %}">
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
</div-->
|
||||
<ul class="nav mr-10">
|
||||
<li class="nav-item">
|
||||
<a onclick="return false;" ng-click="showUploadBox()" class="nav-link point-events" href="#"><i class="fa fa-upload" aria-hidden="true"></i> {% trans "Upload" %}</a>
|
||||
<a id="uploadTriggerBtn" onclick="return false;" ng-click="showUploadBox()" class="nav-link point-events" href="#"><i class="fa fa-upload" aria-hidden="true"></i> {% trans "Upload" %}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a onclick="return false;" ng-click="showCreateFileModal()" class="nav-link point-events" href="#"><i class="fa fa-plus-square" aria-hidden="true"></i> {% trans "New File" %}</a>
|
||||
@@ -709,7 +709,7 @@
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js" integrity="sha384-vFJXuSJphROIrBnz7yo7oB41mKfc8JzQZiCq4NCceLEaO4IHwicKwpJf9c9IpFgh" crossorigin="anonymous"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js" integrity="sha384-alpBpkh1PFOepccYVYDB4do5UnbKysX5WZXm3XxPqe5iKTfUKjNkCk9SaVuEZflJ" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-beta.2/js/bootstrap.min.js" integrity="sha384-alpBpkh1PFOepccYVYDB4do5UnbKysX5WZXm3XxPqe5iKTfUKjNkCk9SaVuEZflJ" crossorigin="anonymous"></script>
|
||||
|
||||
|
||||
<!-- HTML Editor Include -->
|
||||
|
||||
@@ -439,7 +439,7 @@
|
||||
<div class="fm-toolbar">
|
||||
<ul class="nav">
|
||||
<li class="nav-item">
|
||||
<a onclick="return false;" ng-click="showUploadBox()" class="nav-link point-events" href="#">
|
||||
<a id="uploadTriggerBtn" onclick="return false;" ng-click="showUploadBox()" class="nav-link point-events" href="#">
|
||||
<i class="fa fa-upload" aria-hidden="true"></i> {% trans "Upload" %}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -1001,7 +1001,7 @@
|
||||
</div>
|
||||
|
||||
<div class="fm-toolbar-group">
|
||||
<button class="fm-btn primary" ng-click="showUploadBox()">
|
||||
<button id="uploadTriggerBtn" class="fm-btn primary" ng-click="showUploadBox()">
|
||||
<i class="fas fa-cloud-upload-alt"></i>
|
||||
{% trans "Upload" %}
|
||||
</button>
|
||||
|
||||
@@ -210,11 +210,18 @@ def upload(request):
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
|
||||
if ACLManager.checkOwnership(data['domainName'], admin, currentACL) == 1:
|
||||
domainName = data.get('domainName', '')
|
||||
if domainName == '':
|
||||
# Root File Manager: allow only admin
|
||||
if currentACL['admin'] == 1:
|
||||
pass
|
||||
else:
|
||||
return ACLManager.loadErrorJson()
|
||||
elif ACLManager.checkOwnership(domainName, admin, currentACL) == 1:
|
||||
pass
|
||||
else:
|
||||
return ACLManager.loadErrorJson()
|
||||
except:
|
||||
except Exception:
|
||||
return ACLManager.loadErrorJson()
|
||||
|
||||
fm = FM(request, data)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
40
firewall/migrations/0001_initial.py
Normal file
40
firewall/migrations/0001_initial.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# Generated migration for firewall app - BannedIP model
|
||||
# Primary storage for banned IPs is the database; JSON is used only for export/import.
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BannedIP',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('ip_address', models.GenericIPAddressField(db_index=True, unique=True, verbose_name='IP Address')),
|
||||
('reason', models.CharField(max_length=255, verbose_name='Ban Reason')),
|
||||
('duration', models.CharField(default='permanent', max_length=50, verbose_name='Duration')),
|
||||
('banned_on', models.DateTimeField(auto_now_add=True, verbose_name='Banned On')),
|
||||
('expires', models.BigIntegerField(blank=True, null=True, verbose_name='Expires Timestamp')),
|
||||
('active', models.BooleanField(db_index=True, default=True, verbose_name='Active')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Banned IP',
|
||||
'verbose_name_plural': 'Banned IPs',
|
||||
'db_table': 'firewall_bannedips',
|
||||
},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='bannedip',
|
||||
index=models.Index(fields=['ip_address', 'active'], name='fw_bannedip_ip_active_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='bannedip',
|
||||
index=models.Index(fields=['active', 'expires'], name='fw_bannedip_active_exp_idx'),
|
||||
),
|
||||
]
|
||||
@@ -25,15 +25,79 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
|
||||
$scope.rulesLoading = true;
|
||||
$scope.actionFailed = true;
|
||||
$scope.actionSuccess = true;
|
||||
$scope.showExportFormatModal = false;
|
||||
$scope.showImportFormatModal = false;
|
||||
$scope.exportRulesFormat = 'json';
|
||||
$scope.importRulesFormat = 'json';
|
||||
$scope.showModifyRuleModal = false;
|
||||
$scope.modifyRuleData = { id: null, name: '', proto: 'tcp', port: '', ruleIP: '0.0.0.0/0' };
|
||||
|
||||
$scope.canNotAddRule = true;
|
||||
$scope.ruleAdded = true;
|
||||
$scope.couldNotConnect = true;
|
||||
$scope.rulesDetails = false;
|
||||
|
||||
// Banned IPs variables
|
||||
$scope.activeTab = 'rules';
|
||||
// Initialize rules array - prevents "Cannot read 'length' of undefined" when template evaluates rules.length before API loads
|
||||
$scope.rules = [];
|
||||
// Banned IPs variables – tab from hash so we stay on /firewall/ (avoids 404 on servers without /firewall/firewall-rules/)
|
||||
function tabFromHash() {
|
||||
var h = (window.location.hash || '').replace(/^#/, '');
|
||||
return (h === 'banned-ips') ? 'banned' : 'rules';
|
||||
}
|
||||
$scope.activeTab = tabFromHash();
|
||||
$scope.bannedIPs = []; // Initialize as empty array
|
||||
|
||||
// Re-apply tab from hash after load (hash can be set after controller init in some browsers)
|
||||
function applyTabFromHash() {
|
||||
var tab = tabFromHash();
|
||||
if ($scope.activeTab !== tab) {
|
||||
$scope.activeTab = tab;
|
||||
if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); }
|
||||
if (!$scope.$$phase && !$scope.$root.$$phase) { $scope.$apply(); }
|
||||
}
|
||||
}
|
||||
$timeout(applyTabFromHash, 0);
|
||||
if (document.readyState === 'complete') {
|
||||
$timeout(applyTabFromHash, 50);
|
||||
} else {
|
||||
window.addEventListener('load', function() { $timeout(applyTabFromHash, 0); });
|
||||
}
|
||||
|
||||
// Sync tab with hash and load that tab's data on switch
|
||||
$scope.setFirewallTab = function(tab) {
|
||||
$timeout(function() {
|
||||
$scope.activeTab = tab;
|
||||
window.location.hash = (tab === 'banned') ? '#banned-ips' : '#rules';
|
||||
if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); }
|
||||
}, 0);
|
||||
};
|
||||
|
||||
// Back/forward or direct hash change: sync tab and load its data
|
||||
function syncTabFromHash() {
|
||||
var tab = tabFromHash();
|
||||
if ($scope.activeTab !== tab) {
|
||||
$scope.activeTab = tab;
|
||||
if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); }
|
||||
if (!$scope.$$phase && !$scope.$root.$$phase) { $scope.$apply(); }
|
||||
}
|
||||
}
|
||||
window.addEventListener('hashchange', syncTabFromHash);
|
||||
|
||||
// Pagination: Firewall Rules (default 10 per page, options 5–100)
|
||||
$scope.rulesPage = 1;
|
||||
$scope.rulesPageSize = 10;
|
||||
$scope.rulesPageSizeOptions = [5, 10, 20, 30, 50, 100];
|
||||
$scope.rulesTotalCount = 0;
|
||||
|
||||
// Pagination: Banned IPs
|
||||
$scope.bannedPage = 1;
|
||||
$scope.bannedPageSize = 10;
|
||||
$scope.bannedPageSizeOptions = [5, 10, 20, 30, 50, 100];
|
||||
$scope.bannedTotalCount = 0;
|
||||
|
||||
// Modify Banned IP modal state
|
||||
$scope.showModifyModal = false;
|
||||
$scope.modifyBannedIPData = { ip: '', id: null, reason: '', duration: '24h' };
|
||||
|
||||
// Initialize banned IPs array - start as null so template shows empty state
|
||||
// Will be set to array after API call
|
||||
@@ -44,12 +108,46 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
|
||||
$scope.banIP = '';
|
||||
$scope.banReason = '';
|
||||
$scope.banDuration = '24h';
|
||||
$scope.bannedIPSearch = '';
|
||||
$scope.searchBannedIPFilter = function(item) {
|
||||
var q = ($scope.bannedIPSearch || '').toLowerCase().trim();
|
||||
if (!q) return true;
|
||||
var ip = (item.ip || '').toLowerCase();
|
||||
var reason = (item.reason || '').toLowerCase();
|
||||
var status = item.active ? 'active' : 'expired';
|
||||
return ip.indexOf(q) !== -1 || reason.indexOf(q) !== -1 || status.indexOf(q) !== -1;
|
||||
};
|
||||
|
||||
$scope.onBannedSearchChange = function() {
|
||||
if ($scope.bannedSearchTimeout) $timeout.cancel($scope.bannedSearchTimeout);
|
||||
$scope.bannedSearchTimeout = $timeout(function() {
|
||||
$scope.bannedPage = 1;
|
||||
if (typeof populateBannedIPs === 'function') populateBannedIPs();
|
||||
}, 350);
|
||||
};
|
||||
|
||||
$scope.runBannedSearch = function() {
|
||||
$scope.bannedPage = 1;
|
||||
if (typeof populateBannedIPs === 'function') populateBannedIPs();
|
||||
};
|
||||
|
||||
firewallStatus();
|
||||
|
||||
// Load both tabs on init; also load on tab change (watch) so content always shows
|
||||
populateCurrentRecords();
|
||||
|
||||
// Load banned IPs immediately when controller initializes
|
||||
populateBannedIPs();
|
||||
|
||||
$scope.$watch('activeTab', function(newVal, oldVal) {
|
||||
if (newVal === oldVal || !newVal) return;
|
||||
$timeout(function() {
|
||||
try {
|
||||
if (newVal === 'banned' && typeof populateBannedIPs === 'function') populateBannedIPs();
|
||||
else if (newVal === 'rules' && typeof populateCurrentRecords === 'function') populateCurrentRecords();
|
||||
} catch (e) {}
|
||||
}, 0);
|
||||
});
|
||||
|
||||
// Log for debugging
|
||||
console.log('=== FIREWALL CONTROLLER INITIALIZING ===');
|
||||
console.log('Initializing firewall controller, loading banned IPs...');
|
||||
|
||||
@@ -69,14 +167,20 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
|
||||
}
|
||||
};
|
||||
|
||||
console.log('Making request to:', url);
|
||||
var postData = {
|
||||
page: Math.max(1, parseInt($scope.bannedPage, 10) || 1),
|
||||
page_size: Math.max(5, Math.min(100, parseInt($scope.bannedPageSize, 10) || 10)),
|
||||
search: ($scope.bannedIPSearch || '').trim()
|
||||
};
|
||||
console.log('Making request to:', url, 'page:', postData.page, 'page_size:', postData.page_size, 'search:', postData.search);
|
||||
console.log('CSRF Token:', csrfToken ? 'Found (' + csrfToken.substring(0, 10) + '...)' : 'MISSING!');
|
||||
|
||||
$http.post(url, {}, config).then(
|
||||
$http.post(url, postData, config).then(
|
||||
function(response) {
|
||||
var res = (typeof response.data === 'string') ? (function() { try { return JSON.parse(response.data); } catch (e) { return {}; } })() : response.data;
|
||||
console.log('=== API RESPONSE RECEIVED ===');
|
||||
console.log('Response status:', response.status);
|
||||
console.log('Response data:', JSON.stringify(response.data, null, 2));
|
||||
console.log('Response data (parsed):', res);
|
||||
|
||||
$scope.bannedIPsLoading = false;
|
||||
// Reset error flags
|
||||
@@ -84,8 +188,8 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
|
||||
$scope.bannedIPActionSuccess = true;
|
||||
$scope.bannedIPCouldNotConnect = true;
|
||||
|
||||
if (response.data && response.data.status === 1) {
|
||||
var bannedIPsArray = response.data.bannedIPs || [];
|
||||
if (res && res.status === 1) {
|
||||
var bannedIPsArray = res.bannedIPs || [];
|
||||
console.log('Raw bannedIPs from API:', bannedIPsArray);
|
||||
console.log('Banned IPs count:', bannedIPsArray.length);
|
||||
console.log('Is array?', Array.isArray(bannedIPsArray));
|
||||
@@ -99,6 +203,10 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
|
||||
// Assign to scope - Angular $http callbacks already run within $apply
|
||||
console.log('Assigning to scope.bannedIPs...');
|
||||
$scope.bannedIPs = bannedIPsArray;
|
||||
$scope.bannedTotalCount = res.total_count != null ? res.total_count : bannedIPsArray.length;
|
||||
$scope.bannedPage = Math.max(1, res.page != null ? res.page : 1);
|
||||
$scope.bannedPageSize = res.page_size != null ? res.page_size : 10;
|
||||
$scope.bannedPageInput = $scope.bannedPage;
|
||||
console.log('After assignment - scope.bannedIPs:', $scope.bannedIPs);
|
||||
console.log('After assignment - scope.bannedIPs.length:', $scope.bannedIPs ? $scope.bannedIPs.length : 'undefined');
|
||||
console.log('After assignment - activeTab:', $scope.activeTab);
|
||||
@@ -109,10 +217,10 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
|
||||
console.log('=== populateBannedIPs() SUCCESS ===');
|
||||
} else {
|
||||
console.error('ERROR: API returned status !== 1');
|
||||
console.error('Response data:', response.data);
|
||||
console.error('Response data:', res);
|
||||
$scope.bannedIPs = [];
|
||||
$scope.bannedIPActionFailed = false;
|
||||
$scope.bannedIPErrorMessage = (response.data && response.data.error_message) || 'Unknown error';
|
||||
$scope.bannedIPErrorMessage = (res && res.error_message) || 'Unknown error';
|
||||
}
|
||||
},
|
||||
function(error) {
|
||||
@@ -144,6 +252,59 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
|
||||
console.log('$scope.populateBannedIPs() called from template');
|
||||
populateBannedIPs();
|
||||
};
|
||||
|
||||
$scope.goToBannedPage = function(page) {
|
||||
var totalP = Math.max(1, $scope.bannedTotalPages());
|
||||
var p = parseInt(page, 10);
|
||||
if (isNaN(p) || p < 1 || p > totalP) return;
|
||||
$scope.bannedPage = p;
|
||||
populateBannedIPs();
|
||||
};
|
||||
$scope.goToBannedPageByInput = function() {
|
||||
var self = $scope;
|
||||
$timeout(function() {
|
||||
var n = parseInt(self.bannedPageInput, 10);
|
||||
if (isNaN(n) || n < 1) n = self.bannedPage || 1;
|
||||
var maxP = Math.max(1, self.bannedTotalPages());
|
||||
n = Math.min(Math.max(1, n), maxP);
|
||||
self.bannedPageInput = n;
|
||||
self.bannedPage = n;
|
||||
populateBannedIPs();
|
||||
}, 0);
|
||||
};
|
||||
$scope.bannedTotalPages = function() {
|
||||
var size = $scope.bannedPageSize || 10;
|
||||
var total = $scope.bannedTotalCount || ($scope.bannedIPs ? $scope.bannedIPs.length : 0) || 0;
|
||||
return size > 0 ? Math.max(1, Math.ceil(total / size)) : 1;
|
||||
};
|
||||
$scope.bannedRangeStart = function() {
|
||||
var total = $scope.bannedTotalCount || ($scope.bannedIPs ? $scope.bannedIPs.length : 0) || 0;
|
||||
if (total === 0) return 0;
|
||||
var page = Math.max(1, $scope.bannedPage || 1);
|
||||
var size = $scope.bannedPageSize || 10;
|
||||
return (page - 1) * size + 1;
|
||||
};
|
||||
$scope.bannedRangeEnd = function() {
|
||||
var start = $scope.bannedRangeStart();
|
||||
var size = $scope.bannedPageSize || 10;
|
||||
var total = $scope.bannedTotalCount || ($scope.bannedIPs ? $scope.bannedIPs.length : 0) || 0;
|
||||
return total === 0 ? 0 : Math.min(start + size - 1, total);
|
||||
};
|
||||
$scope.setBannedPageSize = function() {
|
||||
var size = parseInt($scope.bannedPageSize, 10);
|
||||
$scope.bannedPageSize = (size >= 5 && size <= 100) ? size : 10;
|
||||
$scope.bannedPage = 1;
|
||||
populateBannedIPs();
|
||||
};
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.__firewallLoadTab = function(tab) {
|
||||
$scope.$evalAsync(function() {
|
||||
$scope.activeTab = tab;
|
||||
if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); }
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Load banned IPs on page load - use $timeout for Angular compatibility
|
||||
// Wrap in try-catch to ensure it executes even if there are other errors
|
||||
@@ -160,33 +321,6 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
|
||||
console.error('Error setting up timeout for populateBannedIPs:', e);
|
||||
}
|
||||
|
||||
// Also load when switching to banned tab - use deep watch for immediate trigger
|
||||
try {
|
||||
$scope.$watch('activeTab', function(newVal, oldVal) {
|
||||
console.log('=== activeTab WATCH TRIGGERED ===');
|
||||
console.log('activeTab changed from', oldVal, 'to', newVal);
|
||||
if (newVal === 'banned') {
|
||||
console.log('Switched to banned IPs tab, calling populateBannedIPs...');
|
||||
// Call immediately
|
||||
try {
|
||||
if (typeof populateBannedIPs === 'function') {
|
||||
console.log('Calling populateBannedIPs from $watch...');
|
||||
populateBannedIPs();
|
||||
} else if (typeof $scope.populateBannedIPs === 'function') {
|
||||
console.log('Calling $scope.populateBannedIPs from $watch...');
|
||||
$scope.populateBannedIPs();
|
||||
} else {
|
||||
console.error('ERROR: populateBannedIPs is not available!');
|
||||
}
|
||||
} catch(e) {
|
||||
console.error('Error calling populateBannedIPs from watch:', e);
|
||||
}
|
||||
}
|
||||
}, true); // Use deep watch (true parameter)
|
||||
} catch(e) {
|
||||
console.error('Error setting up $watch for activeTab:', e);
|
||||
}
|
||||
|
||||
$scope.addRule = function () {
|
||||
|
||||
$scope.rulesLoading = false;
|
||||
@@ -278,39 +412,143 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
|
||||
$scope.actionFailed = true;
|
||||
$scope.actionSuccess = true;
|
||||
|
||||
|
||||
url = "/firewall/getCurrentRules";
|
||||
|
||||
var data = {};
|
||||
|
||||
var data = {
|
||||
page: Math.max(1, parseInt($scope.rulesPage, 10) || 1),
|
||||
page_size: Math.max(5, Math.min(100, parseInt($scope.rulesPageSize, 10) || 10))
|
||||
};
|
||||
var config = {
|
||||
headers: {
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
$http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas);
|
||||
|
||||
|
||||
function ListInitialDatas(response) {
|
||||
if (response.data.fetchStatus === 1) {
|
||||
$scope.rules = JSON.parse(response.data.data);
|
||||
var res = (typeof response.data === 'string') ? (function() { try { return JSON.parse(response.data); } catch (e) { return {}; } })() : response.data;
|
||||
if (res && res.fetchStatus === 1) {
|
||||
$scope.rules = typeof res.data === 'string' ? JSON.parse(res.data) : (res.data || []);
|
||||
$scope.rulesTotalCount = res.total_count != null ? res.total_count : ($scope.rules ? $scope.rules.length : 0);
|
||||
$scope.rulesPage = Math.max(1, res.page != null ? res.page : 1);
|
||||
$scope.rulesPageSize = res.page_size != null ? res.page_size : 10;
|
||||
$scope.rulesLoading = true;
|
||||
}
|
||||
else {
|
||||
$scope.rulesLoading = true;
|
||||
$scope.errorMessage = response.data.error_message;
|
||||
$scope.errorMessage = (res && res.error_message) ? res.error_message : '';
|
||||
}
|
||||
}
|
||||
|
||||
function cantLoadInitialDatas(response) {
|
||||
$scope.couldNotConnect = false;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$scope.goToRulesPage = function(page) {
|
||||
var totalP = Math.max(1, $scope.rulesTotalPages());
|
||||
var p = parseInt(page, 10);
|
||||
if (isNaN(p) || p < 1 || p > totalP) return;
|
||||
$scope.rulesPage = p;
|
||||
populateCurrentRecords();
|
||||
};
|
||||
$scope.goToRulesPageByInput = function() {
|
||||
$timeout(function() {
|
||||
var n = parseInt($scope.rulesPageInput, 10);
|
||||
if (isNaN(n) || n < 1) n = $scope.rulesPage || 1;
|
||||
var maxP = Math.max(1, $scope.rulesTotalPages());
|
||||
n = Math.min(Math.max(1, n), maxP);
|
||||
$scope.rulesPageInput = n;
|
||||
$scope.rulesPage = n;
|
||||
populateCurrentRecords();
|
||||
}, 0);
|
||||
};
|
||||
$scope.rulesTotalPages = function() {
|
||||
var size = $scope.rulesPageSize || 10;
|
||||
var total = $scope.rulesTotalCount || ($scope.rules && $scope.rules.length) || 0;
|
||||
return size > 0 ? Math.max(1, Math.ceil(total / size)) : 1;
|
||||
};
|
||||
$scope.rulesRangeStart = function() {
|
||||
var total = $scope.rulesTotalCount || ($scope.rules && $scope.rules.length) || 0;
|
||||
if (total === 0) return 0;
|
||||
var page = Math.max(1, $scope.rulesPage || 1);
|
||||
var size = $scope.rulesPageSize || 10;
|
||||
return (page - 1) * size + 1;
|
||||
};
|
||||
$scope.rulesRangeEnd = function() {
|
||||
var start = $scope.rulesRangeStart();
|
||||
var size = $scope.rulesPageSize || 10;
|
||||
var total = $scope.rulesTotalCount || ($scope.rules && $scope.rules.length) || 0;
|
||||
return total === 0 ? 0 : Math.min(start + size - 1, total);
|
||||
};
|
||||
$scope.setRulesPageSize = function() {
|
||||
var size = parseInt($scope.rulesPageSize, 10);
|
||||
$scope.rulesPageSize = (size >= 5 && size <= 100) ? size : 10;
|
||||
$scope.rulesPage = 1;
|
||||
populateCurrentRecords();
|
||||
};
|
||||
|
||||
$scope.openModifyRuleModal = function(rule) {
|
||||
if (!rule) return;
|
||||
$scope.modifyRuleData = {
|
||||
id: rule.id,
|
||||
name: rule.name || '',
|
||||
proto: rule.proto || 'tcp',
|
||||
port: String(rule.port || ''),
|
||||
ruleIP: rule.ipAddress || rule.ruleIP || '0.0.0.0/0'
|
||||
};
|
||||
$scope.showModifyRuleModal = true;
|
||||
};
|
||||
|
||||
$scope.closeModifyRuleModal = function() {
|
||||
$scope.showModifyRuleModal = false;
|
||||
$scope.modifyRuleData = { id: null, name: '', proto: 'tcp', port: '', ruleIP: '0.0.0.0/0' };
|
||||
};
|
||||
|
||||
$scope.saveModifyRule = function() {
|
||||
var d = $scope.modifyRuleData;
|
||||
if (!d.name || !d.name.trim()) {
|
||||
$scope.actionFailed = false;
|
||||
$scope.actionSuccess = true;
|
||||
$scope.errorMessage = 'Rule name is required';
|
||||
return;
|
||||
}
|
||||
if (!d.port || !String(d.port).trim()) {
|
||||
$scope.actionFailed = false;
|
||||
$scope.actionSuccess = true;
|
||||
$scope.errorMessage = 'Port is required';
|
||||
return;
|
||||
}
|
||||
$scope.rulesLoading = false;
|
||||
var url = '/firewall/modifyRule';
|
||||
var data = {
|
||||
id: d.id,
|
||||
name: d.name.trim(),
|
||||
proto: d.proto || 'tcp',
|
||||
port: String(d.port).trim(),
|
||||
ruleIP: (d.ruleIP || '0.0.0.0/0').trim()
|
||||
};
|
||||
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } };
|
||||
$http.post(url, data, config).then(function(response) {
|
||||
$scope.rulesLoading = true;
|
||||
if (response.data && response.data.status === 1) {
|
||||
$scope.closeModifyRuleModal();
|
||||
$scope.actionFailed = true;
|
||||
$scope.actionSuccess = false;
|
||||
populateCurrentRecords();
|
||||
} else {
|
||||
$scope.actionFailed = false;
|
||||
$scope.actionSuccess = true;
|
||||
$scope.errorMessage = (response.data && response.data.error_message) || 'Modify failed';
|
||||
}
|
||||
}, function() {
|
||||
$scope.rulesLoading = true;
|
||||
$scope.actionFailed = false;
|
||||
$scope.actionSuccess = true;
|
||||
$scope.errorMessage = 'Could not connect to server. Please refresh this page.';
|
||||
});
|
||||
};
|
||||
|
||||
$scope.deleteRule = function (id, proto, port, ruleIP) {
|
||||
|
||||
$scope.rulesLoading = false;
|
||||
@@ -792,101 +1030,239 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
|
||||
});
|
||||
};
|
||||
|
||||
// Export/Import Firewall Rules Functions
|
||||
$scope.openModifyModal = function(bannedIP) {
|
||||
if (!bannedIP) return;
|
||||
$scope.modifyBannedIPData = {
|
||||
id: bannedIP.id,
|
||||
ip: bannedIP.ip || bannedIP.ip_address || '',
|
||||
reason: bannedIP.reason || '',
|
||||
duration: bannedIP.duration || '24h'
|
||||
};
|
||||
$scope.showModifyModal = true;
|
||||
};
|
||||
|
||||
$scope.closeModifyModal = function() {
|
||||
$scope.showModifyModal = false;
|
||||
$scope.modifyBannedIPData = { ip: '', id: null, reason: '', duration: '24h' };
|
||||
};
|
||||
|
||||
$scope.saveModifyBannedIP = function() {
|
||||
var d = $scope.modifyBannedIPData;
|
||||
if (!d.reason || !d.reason.trim()) {
|
||||
$scope.bannedIPActionFailed = false;
|
||||
$scope.bannedIPErrorMessage = 'Reason is required';
|
||||
return;
|
||||
}
|
||||
$scope.bannedIPsLoading = true;
|
||||
$scope.bannedIPActionFailed = true;
|
||||
$scope.bannedIPActionSuccess = true;
|
||||
$scope.bannedIPCouldNotConnect = true;
|
||||
var data = {
|
||||
id: d.id,
|
||||
ip: d.ip,
|
||||
reason: d.reason.trim(),
|
||||
duration: d.duration || '24h'
|
||||
};
|
||||
var url = '/firewall/modifyBannedIP';
|
||||
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } };
|
||||
$http.post(url, data, config).then(function(response) {
|
||||
$scope.bannedIPsLoading = false;
|
||||
if (response.data && response.data.status === 1) {
|
||||
$scope.bannedIPActionSuccess = false;
|
||||
$scope.closeModifyModal();
|
||||
populateBannedIPs();
|
||||
} else {
|
||||
$scope.bannedIPActionFailed = false;
|
||||
$scope.bannedIPErrorMessage = (response.data && response.data.error_message) || 'Modify failed';
|
||||
}
|
||||
}, function(error) {
|
||||
$scope.bannedIPsLoading = false;
|
||||
$scope.bannedIPCouldNotConnect = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.exportBannedIPs = function() {
|
||||
$scope.bannedIPsLoading = false;
|
||||
var url = "/firewall/exportBannedIPs";
|
||||
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') }, responseType: 'text' };
|
||||
$http.post(url, {}, config).then(function(response) {
|
||||
$scope.bannedIPsLoading = true;
|
||||
var raw = response.data;
|
||||
try {
|
||||
var data = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
||||
if (data && data.exportStatus === 0) {
|
||||
$scope.bannedIPActionFailed = false;
|
||||
$scope.bannedIPErrorMessage = data.error_message || 'Export failed';
|
||||
return;
|
||||
}
|
||||
} catch (e) {}
|
||||
var content = typeof raw === 'string' ? raw : JSON.stringify(raw, null, 2);
|
||||
var blob = new Blob([content], { type: 'application/json' });
|
||||
var a = document.createElement('a');
|
||||
a.href = window.URL.createObjectURL(blob);
|
||||
a.download = 'banned_ips_export_' + (Date.now() / 1000 | 0) + '.json';
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(a.href);
|
||||
$scope.bannedIPActionSuccess = false;
|
||||
}, function() {
|
||||
$scope.bannedIPsLoading = true;
|
||||
$scope.bannedIPCouldNotConnect = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.importBannedIPs = function() {
|
||||
var input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.json';
|
||||
input.style.display = 'none';
|
||||
input.onchange = function(event) {
|
||||
var file = event.target.files[0];
|
||||
if (!file) return;
|
||||
$scope.bannedIPsLoading = false;
|
||||
var formData = new FormData();
|
||||
formData.append('import_file', file);
|
||||
var config = {
|
||||
headers: { 'X-CSRFToken': getCookie('csrftoken'), 'Content-Type': undefined },
|
||||
transformRequest: angular.identity
|
||||
};
|
||||
$http.post("/firewall/importBannedIPs", formData, config).then(function(response) {
|
||||
$scope.bannedIPsLoading = true;
|
||||
if (response.data && response.data.importStatus === 1) {
|
||||
$scope.bannedIPActionSuccess = false;
|
||||
populateBannedIPs();
|
||||
} else {
|
||||
$scope.bannedIPActionFailed = false;
|
||||
$scope.bannedIPErrorMessage = (response.data && response.data.error_message) || 'Import failed';
|
||||
}
|
||||
}, function() {
|
||||
$scope.bannedIPsLoading = true;
|
||||
$scope.bannedIPCouldNotConnect = false;
|
||||
});
|
||||
};
|
||||
document.body.appendChild(input);
|
||||
input.click();
|
||||
document.body.removeChild(input);
|
||||
};
|
||||
|
||||
// Export/Import Firewall Rules: format modals and actions
|
||||
$scope.exportRules = function () {
|
||||
$scope.rulesLoading = false;
|
||||
$scope.showExportFormatModal = true;
|
||||
$scope.exportRulesFormat = $scope.exportRulesFormat || 'json';
|
||||
};
|
||||
|
||||
$scope.closeExportFormatModal = function () {
|
||||
$scope.showExportFormatModal = false;
|
||||
};
|
||||
|
||||
$scope.confirmExportRules = function () {
|
||||
$scope.showExportFormatModal = false;
|
||||
var format = $scope.exportRulesFormat || 'json';
|
||||
doExportRules(format);
|
||||
};
|
||||
|
||||
function doExportRules(format) {
|
||||
$scope.rulesLoading = true;
|
||||
$scope.actionFailed = true;
|
||||
$scope.actionSuccess = true;
|
||||
|
||||
url = "/firewall/exportFirewallRules";
|
||||
|
||||
var data = {};
|
||||
|
||||
var url = "/firewall/exportFirewallRules";
|
||||
var data = { format: format };
|
||||
var config = {
|
||||
headers: {
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
}
|
||||
headers: { 'X-CSRFToken': getCookie('csrftoken') },
|
||||
responseType: 'text'
|
||||
};
|
||||
|
||||
$http.post(url, data, config).then(exportSuccess, exportError);
|
||||
|
||||
function exportSuccess(response) {
|
||||
$scope.rulesLoading = true;
|
||||
|
||||
// Check if response is JSON (error) or file download
|
||||
if (typeof response.data === 'string' && response.data.includes('{')) {
|
||||
$scope.rulesLoading = false;
|
||||
var raw = response.data;
|
||||
if (typeof raw === 'string' && raw.indexOf('{') === 0) {
|
||||
try {
|
||||
var errorData = JSON.parse(response.data);
|
||||
if (errorData.exportStatus === 0) {
|
||||
var parsed = JSON.parse(raw);
|
||||
if (parsed.exportStatus === 0) {
|
||||
$scope.actionFailed = false;
|
||||
$scope.actionSuccess = true;
|
||||
$scope.errorMessage = errorData.error_message;
|
||||
$scope.errorMessage = parsed.error_message || 'Export failed';
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
// If not JSON, assume it's the file content
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// If we get here, it's a successful file download
|
||||
var contentType = format === 'excel' ? 'text/csv' : 'application/json';
|
||||
var ext = format === 'excel' ? 'csv' : 'json';
|
||||
var blob = new Blob([typeof raw === 'string' ? raw : JSON.stringify(raw)], { type: contentType });
|
||||
var a = document.createElement('a');
|
||||
a.href = window.URL.createObjectURL(blob);
|
||||
a.download = 'firewall_rules_export_' + (Date.now() / 1000 | 0) + '.' + ext;
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(a.href);
|
||||
$scope.actionFailed = true;
|
||||
$scope.actionSuccess = false;
|
||||
}
|
||||
|
||||
function exportError(response) {
|
||||
$scope.rulesLoading = true;
|
||||
function exportError() {
|
||||
$scope.rulesLoading = false;
|
||||
$scope.actionFailed = false;
|
||||
$scope.actionSuccess = true;
|
||||
$scope.errorMessage = "Could not connect to server. Please refresh this page.";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
$scope.importRules = function () {
|
||||
// Create file input element
|
||||
$scope.showImportFormatModal = true;
|
||||
$scope.importRulesFormat = $scope.importRulesFormat || 'json';
|
||||
};
|
||||
|
||||
$scope.closeImportFormatModal = function () {
|
||||
$scope.showImportFormatModal = false;
|
||||
};
|
||||
|
||||
$scope.confirmImportRules = function () {
|
||||
$scope.showImportFormatModal = false;
|
||||
var format = $scope.importRulesFormat || 'json';
|
||||
var accept = format === 'excel' ? '.csv' : '.json';
|
||||
var input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.json';
|
||||
input.accept = accept;
|
||||
input.style.display = 'none';
|
||||
|
||||
input.onchange = function(event) {
|
||||
var file = event.target.files[0];
|
||||
if (file) {
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
try {
|
||||
var importData = JSON.parse(e.target.result);
|
||||
|
||||
// Validate file format
|
||||
if (!importData.rules || !Array.isArray(importData.rules)) {
|
||||
if (format === 'json') {
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
try {
|
||||
var importData = JSON.parse(e.target.result);
|
||||
if (!importData.rules || !Array.isArray(importData.rules)) {
|
||||
$scope.$apply(function() {
|
||||
$scope.actionFailed = false;
|
||||
$scope.actionSuccess = true;
|
||||
$scope.errorMessage = "Invalid import file format. Please select a valid firewall rules export file.";
|
||||
});
|
||||
return;
|
||||
}
|
||||
uploadImportFile(file);
|
||||
} catch (err) {
|
||||
$scope.$apply(function() {
|
||||
$scope.actionFailed = false;
|
||||
$scope.actionSuccess = true;
|
||||
$scope.errorMessage = "Invalid import file format. Please select a valid firewall rules export file.";
|
||||
$scope.errorMessage = "Invalid JSON file. Please select a valid firewall rules export file.";
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload file to server
|
||||
uploadImportFile(file);
|
||||
} catch (error) {
|
||||
$scope.$apply(function() {
|
||||
$scope.actionFailed = false;
|
||||
$scope.actionSuccess = true;
|
||||
$scope.errorMessage = "Invalid JSON file. Please select a valid firewall rules export file.";
|
||||
});
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
} else {
|
||||
uploadImportFile(file);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.body.appendChild(input);
|
||||
input.click();
|
||||
document.body.removeChild(input);
|
||||
};
|
||||
|
||||
function uploadImportFile(file) {
|
||||
$scope.rulesLoading = false;
|
||||
$scope.rulesLoading = true;
|
||||
$scope.actionFailed = true;
|
||||
$scope.actionSuccess = true;
|
||||
|
||||
@@ -904,35 +1280,29 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
|
||||
$http.post("/firewall/importFirewallRules", formData, config).then(importSuccess, importError);
|
||||
|
||||
function importSuccess(response) {
|
||||
$scope.rulesLoading = true;
|
||||
|
||||
if (response.data.importStatus === 1) {
|
||||
$scope.rulesLoading = false;
|
||||
var res = response.data;
|
||||
if (typeof res === 'string') {
|
||||
try { res = JSON.parse(res); } catch (e) { res = {}; }
|
||||
}
|
||||
if (res && res.importStatus === 1) {
|
||||
$scope.actionFailed = true;
|
||||
$scope.actionSuccess = false;
|
||||
|
||||
// Refresh rules list
|
||||
populateCurrentRecords();
|
||||
|
||||
// Show import summary
|
||||
var summary = `Import completed successfully!\n` +
|
||||
`Imported: ${response.data.imported_count} rules\n` +
|
||||
`Skipped: ${response.data.skipped_count} rules\n` +
|
||||
`Errors: ${response.data.error_count} rules`;
|
||||
|
||||
if (response.data.errors && response.data.errors.length > 0) {
|
||||
summary += `\n\nErrors:\n${response.data.errors.join('\n')}`;
|
||||
var summary = "Import completed successfully!\nImported: " + (res.imported_count || 0) + " rules\nSkipped: " + (res.skipped_count || 0) + " rules\nErrors: " + (res.error_count || 0) + " rules";
|
||||
if (res.errors && res.errors.length > 0) {
|
||||
summary += "\n\nErrors:\n" + res.errors.join("\n");
|
||||
}
|
||||
|
||||
alert(summary);
|
||||
} else {
|
||||
$scope.actionFailed = false;
|
||||
$scope.actionSuccess = true;
|
||||
$scope.errorMessage = response.data.error_message;
|
||||
$scope.errorMessage = (res && res.error_message) ? res.error_message : "Import failed.";
|
||||
}
|
||||
}
|
||||
|
||||
function importError(response) {
|
||||
$scope.rulesLoading = true;
|
||||
function importError() {
|
||||
$scope.rulesLoading = false;
|
||||
$scope.actionFailed = false;
|
||||
$scope.actionSuccess = true;
|
||||
$scope.errorMessage = "Could not connect to server. Please refresh this page.";
|
||||
@@ -2837,4 +3207,26 @@ app.controller('litespeed_ent_conf', function ($scope, $http, $timeout, $window)
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
(function() {
|
||||
// Do not capture tab clicks – let Angular ng-click run setFirewallTab() so data loads.
|
||||
// Only sync tab from hash on load and hashchange (back/forward) via __firewallLoadTab.
|
||||
function syncFirewallTabFromHash() {
|
||||
var nav = document.getElementById('firewall-tab-nav');
|
||||
if (!nav) return;
|
||||
var h = (window.location.hash || '').replace(/^#/, '');
|
||||
var tab = (h === 'banned-ips') ? 'banned' : 'rules';
|
||||
if (window.__firewallLoadTab) {
|
||||
try { window.__firewallLoadTab(tab); } catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', syncFirewallTabFromHash);
|
||||
} else {
|
||||
syncFirewallTabFromHash();
|
||||
}
|
||||
setTimeout(syncFirewallTabFromHash, 100);
|
||||
window.addEventListener('hashchange', syncFirewallTabFromHash);
|
||||
})();
|
||||
@@ -9,6 +9,10 @@
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
[ng-cloak], .ng-cloak {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
text-align: center;
|
||||
@@ -203,6 +207,8 @@
|
||||
|
||||
/* Rules Panel */
|
||||
.rules-panel {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background: var(--bg-secondary, white);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.05), 0 10px 40px rgba(0,0,0,0.08);
|
||||
@@ -219,6 +225,34 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.panel-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-panel {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
color: var(--text-light, white);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-panel:hover {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
@@ -282,6 +316,15 @@
|
||||
box-shadow: 0 0 0 3px var(--firewall-focus-shadow, rgba(239, 68, 68, 0.1));
|
||||
}
|
||||
|
||||
/* Search input: neutral blue focus (not red) so it doesn't look read-only/error */
|
||||
.firewall-search-input {
|
||||
border-color: var(--border-color, #e2e8f0);
|
||||
}
|
||||
.firewall-search-input:focus {
|
||||
border-color: var(--accent-color, #5b5fcf);
|
||||
box-shadow: 0 0 0 3px rgba(91, 95, 207, 0.15);
|
||||
}
|
||||
|
||||
.select-control {
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
|
||||
@@ -326,6 +369,12 @@
|
||||
border: 1px solid var(--border-color, #e8e9ff);
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.rules-table thead {
|
||||
background: var(--bg-tertiary, #f8f9ff);
|
||||
}
|
||||
@@ -341,6 +390,16 @@
|
||||
border-bottom: 1px solid var(--border-color, #e8e9ff);
|
||||
}
|
||||
|
||||
.rules-table th:last-child,
|
||||
.banned-table th:last-child {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.rules-table td:last-child,
|
||||
.banned-table td:last-child {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.rules-table td {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--border-light, #f1f5f9);
|
||||
@@ -356,6 +415,13 @@
|
||||
background: var(--bg-hover, #f8f9ff);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.rule-id {
|
||||
font-weight: 600;
|
||||
color: var(--text-muted, #64748b);
|
||||
@@ -521,6 +587,134 @@
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Pagination */
|
||||
.pagination-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
padding: 1rem 2rem;
|
||||
border-top: 1px solid var(--border-color, #e8e9ff);
|
||||
background: var(--bg-tertiary, #f8f9ff);
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary, #64748b);
|
||||
}
|
||||
.pagination-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.pagination-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.pagination-controls button {
|
||||
padding: 0.4rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-color, #e2e8f0);
|
||||
background: var(--bg-secondary, white);
|
||||
color: var(--text-primary, #1e293b);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.pagination-controls button:hover:not(:disabled) {
|
||||
background: var(--firewall-accent, #ef4444);
|
||||
color: white;
|
||||
border-color: var(--firewall-accent, #ef4444);
|
||||
}
|
||||
.pagination-controls button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.pagination-controls .page-num {
|
||||
min-width: 2rem;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
color: var(--firewall-accent, #ef4444);
|
||||
}
|
||||
.pagination-size {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.pagination-size select,
|
||||
.pagination-size-select {
|
||||
min-width: 4.5rem;
|
||||
padding: 0.35rem 0.5rem;
|
||||
border: 1px solid var(--border-color, #e2e8f0);
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
background: var(--bg-secondary, white);
|
||||
cursor: pointer;
|
||||
appearance: menulist;
|
||||
display: inline-block;
|
||||
}
|
||||
.banned-ips-per-page select.pagination-size-select {
|
||||
appearance: menulist;
|
||||
display: inline-block;
|
||||
}
|
||||
.pagination-size-btns {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.2rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.pagination-size-btn {
|
||||
min-width: 2rem;
|
||||
padding: 0.35rem 0.5rem;
|
||||
border: 1px solid var(--border-color, #e2e8f0);
|
||||
border-radius: 4px;
|
||||
background: var(--bg-secondary, white);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
.pagination-size-btn:hover {
|
||||
border-color: var(--firewall-accent, #ef4444);
|
||||
color: var(--firewall-accent, #ef4444);
|
||||
}
|
||||
.pagination-size-btn.active {
|
||||
background: var(--firewall-accent, #ef4444);
|
||||
color: white;
|
||||
border-color: var(--firewall-accent, #ef4444);
|
||||
}
|
||||
.pagination-goto {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
.pagination-goto-label {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary, #64748b);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.pagination-goto-input {
|
||||
width: 3.5rem;
|
||||
padding: 0.35rem 0.5rem;
|
||||
border: 1px solid var(--border-color, #e2e8f0);
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
text-align: center;
|
||||
}
|
||||
.pagination-goto-btn {
|
||||
padding: 0.35rem 0.6rem;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border-color, #e2e8f0);
|
||||
background: var(--bg-secondary, white);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
.pagination-goto-btn:hover {
|
||||
background: var(--firewall-accent, #ef4444);
|
||||
color: white;
|
||||
border-color: var(--firewall-accent, #ef4444);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1024px) {
|
||||
.rule-form {
|
||||
@@ -562,7 +756,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Tab Navigation Styles */
|
||||
/* Tab Navigation – always on top, clearly clickable */
|
||||
.tab-navigation {
|
||||
display: flex;
|
||||
background: var(--bg-secondary, white);
|
||||
@@ -571,6 +765,9 @@
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 2px 8px var(--shadow-light, rgba(0,0,0,0.1));
|
||||
border: 1px solid var(--border-color, #e8e9ff);
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
@@ -583,11 +780,16 @@
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
pointer-events: auto;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
text-decoration: none;
|
||||
font-family: inherit;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.tab-button:hover {
|
||||
@@ -595,6 +797,11 @@
|
||||
color: var(--accent-color, #5b5fcf);
|
||||
}
|
||||
|
||||
.tab-button:focus {
|
||||
outline: 2px solid var(--accent-color, #5b5fcf);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.tab-button.tab-active {
|
||||
background: var(--accent-color, #5b5fcf);
|
||||
color: var(--bg-secondary, white);
|
||||
@@ -603,10 +810,13 @@
|
||||
|
||||
.tab-button i {
|
||||
font-size: 1rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Banned IPs Panel Styles */
|
||||
/* Banned IPs Panel – below tab bar (z-index) */
|
||||
.banned-ips-panel {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background: var(--bg-secondary, white);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 12px var(--shadow-medium, rgba(0,0,0,0.15));
|
||||
@@ -648,6 +858,27 @@
|
||||
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4);
|
||||
}
|
||||
|
||||
.btn-search {
|
||||
background: var(--accent-color, #5b5fcf);
|
||||
color: var(--bg-secondary, white);
|
||||
border: none;
|
||||
padding: 0.75rem 1.25rem;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-search:hover {
|
||||
background: var(--accent-hover, #4f46e5);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(91, 95, 207, 0.4);
|
||||
}
|
||||
|
||||
.banned-list-section {
|
||||
padding: 2rem;
|
||||
}
|
||||
@@ -747,6 +978,7 @@
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-unban {
|
||||
@@ -791,8 +1023,147 @@
|
||||
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.btn-modify {
|
||||
background: var(--accent-color, #5b5fcf);
|
||||
color: var(--bg-secondary, white);
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.btn-modify:hover {
|
||||
background: var(--accent-hover, #4f46e5);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(91, 95, 207, 0.3);
|
||||
}
|
||||
|
||||
.modify-modal-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 9999;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modify-modal-overlay.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modify-modal {
|
||||
background: var(--bg-secondary, white);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
max-width: 480px;
|
||||
width: 90%;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.modify-modal h3 {
|
||||
margin: 0 0 1.5rem 0;
|
||||
font-size: 1.25rem;
|
||||
color: var(--text-primary, #1e293b);
|
||||
}
|
||||
|
||||
.modify-modal .form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.modify-modal .form-label {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-secondary, #64748b);
|
||||
}
|
||||
|
||||
.modify-modal .form-control {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--border-color, #e2e8f0);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.modify-modal-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: flex-end;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.modify-modal-actions .btn-cancel {
|
||||
background: #e2e8f0;
|
||||
color: #64748b;
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modify-modal-actions .btn-save {
|
||||
background: var(--accent-color, #5b5fcf);
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Responsive Design for Banned IPs */
|
||||
@media (max-width: 768px) {
|
||||
.status-content {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rule-form {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.rules-table-section {
|
||||
padding: 1rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.rules-table {
|
||||
min-width: 720px;
|
||||
}
|
||||
|
||||
.banned-search-section input {
|
||||
max-width: 100% !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.panel-actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-panel {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.banned-form {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
@@ -885,25 +1256,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab Navigation -->
|
||||
<div class="tab-navigation">
|
||||
<button type="button"
|
||||
ng-click="activeTab = 'rules'"
|
||||
ng-class="{'tab-active': activeTab === 'rules'}"
|
||||
class="tab-button">
|
||||
<!-- Tab Navigation: buttons with native fallback so clicks always work -->
|
||||
<div class="tab-navigation" role="tablist" id="firewall-tab-nav">
|
||||
<button type="button" class="tab-button" role="tab" data-tab="rules" title="{% trans 'Firewall Rules' %}" ng-class="{'tab-active': activeTab === 'rules'}" ng-click="setFirewallTab('rules')">
|
||||
<i class="fas fa-list-alt"></i>
|
||||
{% trans "Firewall Rules" %}
|
||||
</button>
|
||||
<button type="button"
|
||||
ng-click="activeTab = 'banned'; populateBannedIPs();"
|
||||
ng-class="{'tab-active': activeTab === 'banned'}"
|
||||
class="tab-button">
|
||||
<button type="button" class="tab-button" role="tab" data-tab="banned" title="{% trans 'Banned IPs' %}" ng-class="{'tab-active': activeTab === 'banned'}" ng-click="setFirewallTab('banned')">
|
||||
<i class="fas fa-ban"></i>
|
||||
{% trans "Banned IPs" %}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Rules Panel -->
|
||||
<!-- Rules Panel (ng-show so panel is always in DOM; visibility toggled by tab) -->
|
||||
<div class="rules-panel" ng-show="activeTab === 'rules'">
|
||||
<div class="panel-header">
|
||||
<div class="panel-title">
|
||||
@@ -932,6 +1297,50 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Export Rules Format Modal -->
|
||||
<div class="modify-modal-overlay" ng-class="{'show': showExportFormatModal}" ng-click="closeExportFormatModal()">
|
||||
<div class="modify-modal" ng-click="$event.stopPropagation()">
|
||||
<h3>{% trans "Export Firewall Rules" %}</h3>
|
||||
<p style="margin: 0 0 1rem 0; color: var(--text-secondary, #64748b);">{% trans "Choose export format:" %}</p>
|
||||
<div class="form-group">
|
||||
<label class="form-label">
|
||||
<input type="radio" ng-model="exportRulesFormat" value="json"> {% trans "JSON" %}
|
||||
</label>
|
||||
<label class="form-label">
|
||||
<input type="radio" ng-model="exportRulesFormat" value="excel"> {% trans "Excel (CSV)" %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="modify-modal-actions">
|
||||
<button type="button" class="btn-cancel" ng-click="closeExportFormatModal()">{% trans "Cancel" %}</button>
|
||||
<button type="button" class="btn-save" ng-click="confirmExportRules()">
|
||||
<i class="fas fa-download"></i> {% trans "Export" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Import Rules Format Modal -->
|
||||
<div class="modify-modal-overlay" ng-class="{'show': showImportFormatModal}" ng-click="closeImportFormatModal()">
|
||||
<div class="modify-modal" ng-click="$event.stopPropagation()">
|
||||
<h3>{% trans "Import Firewall Rules" %}</h3>
|
||||
<p style="margin: 0 0 1rem 0; color: var(--text-secondary, #64748b);">{% trans "Choose file format:" %}</p>
|
||||
<div class="form-group">
|
||||
<label class="form-label">
|
||||
<input type="radio" ng-model="importRulesFormat" value="json"> {% trans "JSON" %}
|
||||
</label>
|
||||
<label class="form-label">
|
||||
<input type="radio" ng-model="importRulesFormat" value="excel"> {% trans "Excel (CSV)" %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="modify-modal-actions">
|
||||
<button type="button" class="btn-cancel" ng-click="closeImportFormatModal()">{% trans "Cancel" %}</button>
|
||||
<button type="button" class="btn-save" ng-click="confirmImportRules()">
|
||||
<i class="fas fa-upload"></i> {% trans "Choose file" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Rule Section -->
|
||||
<div ng-hide="rulesDetails" class="add-rule-section">
|
||||
@@ -982,7 +1391,8 @@
|
||||
|
||||
<!-- Rules Table -->
|
||||
<div ng-hide="rulesDetails" class="rules-table-section">
|
||||
<table class="rules-table" ng-if="rules.length > 0">
|
||||
<div class="table-responsive">
|
||||
<table class="rules-table" ng-if="rules && rules.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "ID" %}</th>
|
||||
@@ -1010,6 +1420,13 @@
|
||||
<span class="port-number">{$ rule.port $}</span>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
ng-click="$parent.openModifyRuleModal(rule); $event.stopPropagation()"
|
||||
class="btn-modify"
|
||||
title="{% trans 'Modify Rule' %}">
|
||||
<i class="fas fa-edit"></i>
|
||||
{% trans "Modify" %}
|
||||
</button>
|
||||
<button type="button"
|
||||
ng-click="deleteRule(rule.id, rule.proto, rule.port, rule.ipAddress)"
|
||||
class="btn-delete"
|
||||
@@ -1019,10 +1436,70 @@
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Modify Firewall Rule Modal -->
|
||||
<div class="modify-modal-overlay" ng-class="{'show': showModifyRuleModal}" ng-click="closeModifyRuleModal()">
|
||||
<div class="modify-modal" ng-click="$event.stopPropagation()">
|
||||
<h3>{% trans "Modify Firewall Rule" %}</h3>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{% trans "Rule Name" %}</label>
|
||||
<input type="text" class="form-control" ng-model="modifyRuleData.name" placeholder="{% trans 'e.g., Allow SSH' %}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{% trans "Protocol" %}</label>
|
||||
<select ng-model="modifyRuleData.proto" class="form-control">
|
||||
<option value="tcp">TCP</option>
|
||||
<option value="udp">UDP</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{% trans "IP Address" %}</label>
|
||||
<input type="text" class="form-control" ng-model="modifyRuleData.ruleIP" placeholder="0.0.0.0/0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{% trans "Port" %}</label>
|
||||
<input type="text" class="form-control" ng-model="modifyRuleData.port" placeholder="e.g., 80">
|
||||
</div>
|
||||
<div class="modify-modal-actions">
|
||||
<button type="button" class="btn-cancel" ng-click="closeModifyRuleModal()">{% trans "Cancel" %}</button>
|
||||
<button type="button" class="btn-save" ng-click="saveModifyRule()">
|
||||
<i class="fas fa-save"></i> {% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Firewall Rules Pagination (show when there are rules or a total count) -->
|
||||
<div class="pagination-bar" ng-if="(rules && rules.length > 0) || (rulesTotalCount > 0)">
|
||||
<div class="pagination-info">
|
||||
<span>{% trans "Showing" %} {$ rulesRangeStart() || 0 $} – {$ rulesRangeEnd() || 0 $} {% trans "of" %} {$ rulesTotalCount || rules.length || 0 $}</span>
|
||||
<span class="pagination-size">
|
||||
<label class="pagination-goto-label">{% trans "Per page:" %}</label>
|
||||
<select ng-model="rulesPageSize" ng-change="setRulesPageSize()" class="pagination-size-select" title="{% trans 'Items per page' %}">
|
||||
<option ng-repeat="n in rulesPageSizeOptions" ng-value="n">{$ n $}</option>
|
||||
</select>
|
||||
</span>
|
||||
</div>
|
||||
<div class="pagination-controls">
|
||||
<button type="button" ng-click="goToRulesPage(rulesPage - 1)" ng-disabled="rulesPage <= 1" title="{% trans 'Previous' %}">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</button>
|
||||
<span class="page-num">{$ rulesPage $} / {$ rulesTotalPages() $}</span>
|
||||
<button type="button" ng-click="goToRulesPage(rulesPage + 1)" ng-disabled="rulesPage >= rulesTotalPages()" title="{% trans 'Next' %}">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</button>
|
||||
<span class="pagination-goto">
|
||||
<label class="pagination-goto-label">{% trans "Go to page" %}</label>
|
||||
<input type="number" min="1" ng-attr-max="rulesTotalPages()" ng-model="rulesPageInput" class="pagination-goto-input" placeholder="{$ rulesPage $}" ng-keypress="$event.keyCode === 13 && goToRulesPageByInput()">
|
||||
<button type="button" class="pagination-goto-btn" ng-click="goToRulesPageByInput()" title="{% trans 'Go' %}">{% trans "Go" %}</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div ng-if="rules.length == 0" class="empty-state">
|
||||
<div ng-if="!rules || rules.length == 0" class="empty-state">
|
||||
<i class="fas fa-shield-alt empty-icon"></i>
|
||||
<h3 class="empty-title">{% trans "No Firewall Rules" %}</h3>
|
||||
<p class="empty-text">{% trans "Add your first firewall rule to control network access to your server." %}</p>
|
||||
@@ -1057,7 +1534,17 @@
|
||||
</div>
|
||||
{% trans "Banned IP Addresses" %}
|
||||
</div>
|
||||
<div ng-show="bannedIPsLoading" class="loading-spinner"></div>
|
||||
<div class="panel-actions">
|
||||
<button type="button" class="btn-panel" ng-click="exportBannedIPs()">
|
||||
<i class="fas fa-file-export"></i>
|
||||
{% trans "Export Banned IPs" %}
|
||||
</button>
|
||||
<button type="button" class="btn-panel" ng-click="importBannedIPs()">
|
||||
<i class="fas fa-file-import"></i>
|
||||
{% trans "Import Banned IPs" %}
|
||||
</button>
|
||||
<div ng-show="bannedIPsLoading" class="loading-spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Banned IP Section -->
|
||||
@@ -1101,9 +1588,30 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Search Banned IPs -->
|
||||
<div class="banned-search-section" style="margin: 1rem 2rem 0;">
|
||||
<label class="form-label" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">
|
||||
<i class="fas fa-search"></i> {% trans "Search banned IPs" %}
|
||||
</label>
|
||||
<div style="display: inline-flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;">
|
||||
<input type="text"
|
||||
class="form-control firewall-search-input"
|
||||
ng-model="bannedIPSearch"
|
||||
ng-change="onBannedSearchChange()"
|
||||
ng-keypress="$event.keyCode === 13 && runBannedSearch(); $event.preventDefault()"
|
||||
placeholder="{% trans 'Search by IP address, reason or status (Active/Expired)...' %}"
|
||||
autocomplete="off"
|
||||
style="max-width: 400px;">
|
||||
<button type="button" class="btn-search" ng-click="runBannedSearch()" title="{% trans 'Run search' %}">
|
||||
<i class="fas fa-search"></i> {% trans "Search" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Banned IPs List -->
|
||||
<div class="banned-list-section" ng-init="activeTab === 'banned' && populateBannedIPs()">
|
||||
<table class="banned-table" ng-if="bannedIPs && bannedIPs.length > 0">
|
||||
<div class="table-responsive">
|
||||
<table class="banned-table" ng-if="bannedIPs && bannedIPs.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "IP Address" %}</th>
|
||||
@@ -1115,7 +1623,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="bannedIP in bannedIPs">
|
||||
<tr ng-repeat="bannedIP in bannedIPs | filter:searchBannedIPFilter">
|
||||
<td class="ip-address">
|
||||
<i class="fas fa-globe"></i>
|
||||
{$ bannedIP.ip $}
|
||||
@@ -1141,7 +1649,15 @@
|
||||
</td>
|
||||
<td class="actions">
|
||||
<button type="button"
|
||||
ng-click="removeBannedIP(bannedIP.id, bannedIP.ip)"
|
||||
ng-click="$parent.openModifyModal(bannedIP); $event.stopPropagation()"
|
||||
class="btn-modify"
|
||||
title="{% trans 'Modify Ban' %}"
|
||||
ng-if="bannedIP.active">
|
||||
<i class="fas fa-edit"></i>
|
||||
{% trans "Modify" %}
|
||||
</button>
|
||||
<button type="button"
|
||||
ng-click="$parent.removeBannedIP(bannedIP.id, bannedIP.ip); $event.stopPropagation()"
|
||||
class="btn-unban"
|
||||
title="{% trans 'Unban IP' %}"
|
||||
ng-if="bannedIP.active">
|
||||
@@ -1149,7 +1665,7 @@
|
||||
{% trans "Unban" %}
|
||||
</button>
|
||||
<button type="button"
|
||||
ng-click="deleteBannedIP(bannedIP.id, bannedIP.ip)"
|
||||
ng-click="$parent.deleteBannedIP(bannedIP.id, bannedIP.ip); $event.stopPropagation()"
|
||||
class="btn-delete"
|
||||
title="{% trans 'Delete Record' %}">
|
||||
<i class="fas fa-trash"></i>
|
||||
@@ -1157,34 +1673,177 @@
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div ng-if="!bannedIPs || bannedIPs.length == 0" class="empty-state">
|
||||
<!-- Banned IPs Pagination (show when there are rows or a total count) -->
|
||||
<div class="pagination-bar" ng-if="(bannedIPs && bannedIPs.length > 0) || (bannedTotalCount > 0)">
|
||||
<div class="pagination-info">
|
||||
<span>{% trans "Showing" %} {$ bannedRangeStart() || 0 $} – {$ bannedRangeEnd() || 0 $} {% trans "of" %} {$ bannedTotalCount || bannedIPs.length || 0 $}</span>
|
||||
<span class="pagination-size banned-ips-per-page">
|
||||
<label class="pagination-goto-label">{% trans "Per page:" %}</label>
|
||||
<select ng-model="bannedPageSize" ng-change="setBannedPageSize()" class="pagination-size-select" title="{% trans 'Items per page' %}" aria-label="{% trans 'Items per page' %}">
|
||||
<option ng-repeat="n in bannedPageSizeOptions" ng-value="n">{$ n $}</option>
|
||||
</select>
|
||||
</span>
|
||||
</div>
|
||||
<div class="pagination-controls">
|
||||
<button type="button" ng-click="goToBannedPage(bannedPage - 1)" ng-disabled="bannedPage <= 1" title="{% trans 'Previous' %}">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</button>
|
||||
<span class="page-num">{$ bannedPage $} / {$ bannedTotalPages() $}</span>
|
||||
<button type="button" ng-click="goToBannedPage(bannedPage + 1)" ng-disabled="bannedPage >= bannedTotalPages()" title="{% trans 'Next' %}">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</button>
|
||||
<span class="pagination-goto">
|
||||
<label class="pagination-goto-label">{% trans "Go to page" %}</label>
|
||||
<input type="number" min="1" ng-attr-max="bannedTotalPages()" ng-model="bannedPageInput" class="pagination-goto-input" placeholder="{$ bannedPage $}" ng-keypress="$event.keyCode === 13 && goToBannedPageByInput(); $event.preventDefault()" aria-label="{% trans 'Go to page' %}">
|
||||
<button type="button" class="pagination-goto-btn" ng-click="goToBannedPageByInput()" title="{% trans 'Go' %}">{% trans "Go" %}</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State: no banned IPs at all (only when not searching) -->
|
||||
<div ng-if="(!bannedIPs || bannedIPs.length == 0) && (bannedIPSearch || '').length === 0" class="empty-state">
|
||||
<i class="fas fa-shield-check empty-icon"></i>
|
||||
<h3 class="empty-title">{% trans "No Banned IPs" %}</h3>
|
||||
<p class="empty-text">{% trans "All IP addresses are currently allowed. Add banned IPs to block suspicious or malicious traffic." %}</p>
|
||||
</div>
|
||||
<!-- Empty State: search returned no matches (server or client filter) -->
|
||||
<div ng-if="(bannedIPSearch || '').length > 0 && ((!bannedIPs || bannedIPs.length == 0) || (bannedIPs | filter:searchBannedIPFilter).length === 0)" class="empty-state">
|
||||
<i class="fas fa-search empty-icon"></i>
|
||||
<h3 class="empty-title">{% trans "No matching banned IPs" %}</h3>
|
||||
<p class="empty-text">{% trans "No banned IPs match your search. Try a different IP, reason or status (Active/Expired)." %}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Messages -->
|
||||
<div style="padding: 0 2rem 2rem;">
|
||||
<div ng-show="bannedIPActionFailed === false" class="alert alert-danger">
|
||||
<!-- Messages: only one visible at a time; use ng-if so nothing shows until we have a message -->
|
||||
<div style="padding: 0 2rem 2rem;" ng-if="bannedIPActionFailed === false || bannedIPActionSuccess === false || bannedIPCouldNotConnect === false">
|
||||
<div ng-if="bannedIPActionFailed === false" class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-circle alert-icon"></i>
|
||||
<span>{% trans "Action failed. Error message:" %} {$ bannedIPErrorMessage $}</span>
|
||||
<span>{% trans "Action failed. Error message:" %} <span ng-bind="bannedIPErrorMessage"></span></span>
|
||||
</div>
|
||||
|
||||
<div ng-show="bannedIPActionSuccess === false" class="alert alert-success">
|
||||
<div ng-if="bannedIPActionFailed !== false && bannedIPActionSuccess === false" class="alert alert-success">
|
||||
<i class="fas fa-check-circle alert-icon"></i>
|
||||
<span>{% trans "Action completed successfully." %}</span>
|
||||
</div>
|
||||
|
||||
<div ng-show="bannedIPCouldNotConnect === false" class="alert alert-danger">
|
||||
<div ng-if="bannedIPActionFailed !== false && bannedIPCouldNotConnect === false" class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-circle alert-icon"></i>
|
||||
<span>{% trans "Could not connect to server. Please refresh this page." %}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modify Banned IP Modal -->
|
||||
<div id="modifyBannedIPModal" class="modify-modal-overlay" ng-class="{'show': showModifyModal}" ng-click="closeModifyModal()">
|
||||
<div class="modify-modal" ng-click="$event.stopPropagation()">
|
||||
<h3>{% trans "Modify Banned IP" %}</h3>
|
||||
<p style="margin: 0 0 1rem 0; color: var(--text-secondary, #64748b);">
|
||||
<strong>{% trans "IP Address" %}:</strong> {$ modifyBannedIPData.ip $}
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{% trans "Reason" %}</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
ng-model="modifyBannedIPData.reason"
|
||||
placeholder="{% trans 'e.g., Suspicious activity' %}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{% trans "Duration" %}</label>
|
||||
<select ng-model="modifyBannedIPData.duration" class="form-control">
|
||||
<option value="1h">{% trans "1 Hour" %}</option>
|
||||
<option value="24h">{% trans "24 Hours" %}</option>
|
||||
<option value="7d">{% trans "7 Days" %}</option>
|
||||
<option value="30d">{% trans "30 Days" %}</option>
|
||||
<option value="permanent">{% trans "Permanent" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="modify-modal-actions">
|
||||
<button type="button" class="btn-cancel" ng-click="closeModifyModal()">
|
||||
{% trans "Cancel" %}
|
||||
</button>
|
||||
<button type="button" class="btn-save" ng-click="saveModifyBannedIP()">
|
||||
<i class="fas fa-save"></i> {% trans "Save Changes" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
var nav = document.getElementById('firewall-tab-nav');
|
||||
if (!nav) return;
|
||||
|
||||
function loadTabViaAngularScope(tab) {
|
||||
if (!window.angular) return false;
|
||||
var container = document.querySelector('.modern-container[ng-controller="firewallController"]') || document.querySelector('.modern-container');
|
||||
if (!container) return false;
|
||||
try {
|
||||
var scope = window.angular.element(container).scope();
|
||||
if (!scope) return false;
|
||||
scope.$evalAsync(function() {
|
||||
scope.activeTab = tab;
|
||||
if (tab === 'banned' && scope.populateBannedIPs) scope.populateBannedIPs();
|
||||
else if (tab === 'rules' && scope.populateCurrentRecords) scope.populateCurrentRecords();
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function loadTab(tab) {
|
||||
if (!tab || (tab !== 'rules' && tab !== 'banned')) return;
|
||||
var done = false;
|
||||
if (window.__firewallLoadTab) {
|
||||
try { window.__firewallLoadTab(tab); done = true; } catch (e) {}
|
||||
}
|
||||
if (!done) {
|
||||
done = loadTabViaAngularScope(tab);
|
||||
}
|
||||
if (!done) {
|
||||
setTimeout(function() {
|
||||
if (window.__firewallLoadTab) { try { window.__firewallLoadTab(tab); } catch (e) {} }
|
||||
else { loadTabViaAngularScope(tab); }
|
||||
}, 50);
|
||||
setTimeout(function() {
|
||||
if (window.__firewallLoadTab) { try { window.__firewallLoadTab(tab); } catch (e) {} }
|
||||
else { loadTabViaAngularScope(tab); }
|
||||
}, 200);
|
||||
setTimeout(function() {
|
||||
if (window.__firewallLoadTab) { try { window.__firewallLoadTab(tab); } catch (e) {} }
|
||||
else { loadTabViaAngularScope(tab); }
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
function onTabButtonClick(btn) {
|
||||
var tab = btn && btn.getAttribute('data-tab');
|
||||
if (!tab) return;
|
||||
window.location.hash = (tab === 'banned') ? '#banned-ips' : '#rules';
|
||||
loadTab(tab);
|
||||
}
|
||||
nav.addEventListener('click', function(e) {
|
||||
var btn = (e.target && e.target.closest) ? e.target.closest('.tab-button[data-tab]') : null;
|
||||
if (btn && nav.contains(btn)) onTabButtonClick(btn);
|
||||
}, false);
|
||||
nav.addEventListener('mousedown', function(e) {
|
||||
var btn = (e.target && e.target.closest) ? e.target.closest('.tab-button[data-tab]') : null;
|
||||
if (btn && nav.contains(btn)) onTabButtonClick(btn);
|
||||
}, false);
|
||||
|
||||
function loadTabFromHash() {
|
||||
var h = (window.location.hash || '').replace(/^#/, '');
|
||||
var tab = (h === 'banned-ips') ? 'banned' : 'rules';
|
||||
loadTab(tab);
|
||||
}
|
||||
var h = (window.location.hash || '').replace(/^#/, '');
|
||||
if (h === 'banned-ips') loadTabFromHash();
|
||||
window.addEventListener('hashchange', loadTabFromHash);
|
||||
setTimeout(loadTabFromHash, 150);
|
||||
setTimeout(loadTabFromHash, 500);
|
||||
})();
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
@@ -3,7 +3,11 @@ from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('securityHome', views.securityHome, name='securityHome'),
|
||||
path('', views.firewallHome, name='firewallHome'),
|
||||
path('firewall-rules/', views.firewallHome, name='firewallRules'),
|
||||
path('firewall-rules', views.firewallHome, name='firewallRulesNoSlash'),
|
||||
path('banned-ips/', views.firewallHome, name='firewallBannedIPs'),
|
||||
path('banned-ips', views.firewallHome, name='firewallBannedIPsNoSlash'),
|
||||
path('', views.firewallHome, name='firewallHome'), # /firewall/ also serves the page so 404 is avoided
|
||||
path('getCurrentRules', views.getCurrentRules, name='getCurrentRules'),
|
||||
path('addRule', views.addRule, name='addRule'),
|
||||
path('modifyRule', views.modifyRule, name='modifyRule'),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from django.shortcuts import redirect
|
||||
from django.http import HttpResponse
|
||||
import json
|
||||
from loginSystem.views import loadLoginPage
|
||||
from plogical.processUtilities import ProcessUtilities
|
||||
@@ -18,6 +19,16 @@ def securityHome(request):
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
|
||||
def firewallRedirect(request):
|
||||
"""Redirect /firewall/ to /firewall/firewall-rules/ so the default tab has a clear URL."""
|
||||
try:
|
||||
if request.session.get('userID'):
|
||||
return redirect('/firewall/firewall-rules/')
|
||||
return redirect(loadLoginPage)
|
||||
except Exception:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
|
||||
def firewallHome(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
@@ -41,7 +52,14 @@ def getCurrentRules(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
fm = FirewallManager()
|
||||
return fm.getCurrentRules(userID)
|
||||
try:
|
||||
body = request.body
|
||||
if isinstance(body, bytes):
|
||||
body = body.decode('utf-8')
|
||||
data = json.loads(body) if body and body.strip() else {}
|
||||
except (json.JSONDecodeError, Exception):
|
||||
data = {}
|
||||
return fm.getCurrentRules(userID, data)
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
@@ -663,47 +681,137 @@ def getBannedIPs(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
fm = FirewallManager()
|
||||
return fm.getBannedIPs(userID)
|
||||
data = {}
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
body = request.body
|
||||
if isinstance(body, bytes):
|
||||
body = body.decode('utf-8')
|
||||
data = json.loads(body) if body and body.strip() else {}
|
||||
except (json.JSONDecodeError, Exception):
|
||||
pass
|
||||
# GET also supported (no body); pagination uses defaults
|
||||
result = fm.getBannedIPs(userID, data)
|
||||
# Ensure we return JSON (FirewallManager may return HttpResponse)
|
||||
return result
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
final_dic = {'status': 0, 'error_message': 'Session expired. Please log in again.', 'bannedIPs': [], 'total_count': 0}
|
||||
return HttpResponse(json.dumps(final_dic), content_type='application/json', status=403)
|
||||
|
||||
def addBannedIP(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
fm = FirewallManager()
|
||||
return fm.addBannedIP(userID, json.loads(request.body))
|
||||
try:
|
||||
body = request.body
|
||||
if isinstance(body, bytes):
|
||||
body = body.decode('utf-8')
|
||||
request_data = json.loads(body) if body and body.strip() else {}
|
||||
except json.JSONDecodeError as e:
|
||||
final_dic = {'status': 0, 'error_message': 'Invalid JSON in request: %s' % str(e)}
|
||||
return HttpResponse(json.dumps(final_dic), content_type='application/json', status=400)
|
||||
except Exception as e:
|
||||
final_dic = {'status': 0, 'error_message': 'Error parsing request: %s' % str(e)}
|
||||
return HttpResponse(json.dumps(final_dic), content_type='application/json', status=400)
|
||||
result = fm.addBannedIP(userID, request_data)
|
||||
return result
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
final_dic = {'status': 0, 'error_message': 'Session expired. Please refresh the page and try again.'}
|
||||
return HttpResponse(json.dumps(final_dic), content_type='application/json', status=403)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as _log
|
||||
error_trace = traceback.format_exc()
|
||||
_log.writeToFile('Error in addBannedIP view: %s\n%s' % (str(e), error_trace))
|
||||
final_dic = {'status': 0, 'error_message': 'Server error: %s' % str(e)}
|
||||
return HttpResponse(json.dumps(final_dic), content_type='application/json', status=500)
|
||||
|
||||
def modifyBannedIP(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
fm = FirewallManager()
|
||||
return fm.modifyBannedIP(userID, json.loads(request.body))
|
||||
try:
|
||||
body = request.body
|
||||
if isinstance(body, bytes):
|
||||
body = body.decode('utf-8')
|
||||
request_data = json.loads(body) if body and body.strip() else {}
|
||||
except json.JSONDecodeError as e:
|
||||
final_dic = {'status': 0, 'error_message': 'Invalid JSON in request: %s' % str(e)}
|
||||
return HttpResponse(json.dumps(final_dic), content_type='application/json', status=400)
|
||||
except Exception as e:
|
||||
final_dic = {'status': 0, 'error_message': 'Error parsing request: %s' % str(e)}
|
||||
return HttpResponse(json.dumps(final_dic), content_type='application/json', status=400)
|
||||
return fm.modifyBannedIP(userID, request_data)
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
final_dic = {'status': 0, 'error_message': 'Session expired. Please refresh the page and try again.'}
|
||||
return HttpResponse(json.dumps(final_dic), content_type='application/json', status=403)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as _log
|
||||
error_trace = traceback.format_exc()
|
||||
_log.writeToFile('Error in modifyBannedIP view: %s\n%s' % (str(e), error_trace))
|
||||
final_dic = {'status': 0, 'error_message': 'Server error: %s' % str(e)}
|
||||
return HttpResponse(json.dumps(final_dic), content_type='application/json', status=500)
|
||||
|
||||
def removeBannedIP(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
fm = FirewallManager()
|
||||
return fm.removeBannedIP(userID, json.loads(request.body))
|
||||
try:
|
||||
body = request.body
|
||||
if isinstance(body, bytes):
|
||||
body = body.decode('utf-8')
|
||||
request_data = json.loads(body) if body and body.strip() else {}
|
||||
except json.JSONDecodeError as e:
|
||||
final_dic = {'status': 0, 'error_message': 'Invalid JSON in request: %s' % str(e)}
|
||||
return HttpResponse(json.dumps(final_dic), content_type='application/json', status=400)
|
||||
except Exception as e:
|
||||
final_dic = {'status': 0, 'error_message': 'Error parsing request: %s' % str(e)}
|
||||
return HttpResponse(json.dumps(final_dic), content_type='application/json', status=400)
|
||||
return fm.removeBannedIP(userID, request_data)
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
final_dic = {'status': 0, 'error_message': 'Session expired. Please refresh the page and try again.'}
|
||||
return HttpResponse(json.dumps(final_dic), content_type='application/json', status=403)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as _log
|
||||
error_trace = traceback.format_exc()
|
||||
_log.writeToFile('Error in removeBannedIP view: %s\n%s' % (str(e), error_trace))
|
||||
final_dic = {'status': 0, 'error_message': 'Server error: %s' % str(e)}
|
||||
return HttpResponse(json.dumps(final_dic), content_type='application/json', status=500)
|
||||
|
||||
def deleteBannedIP(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
fm = FirewallManager()
|
||||
return fm.deleteBannedIP(userID, json.loads(request.body))
|
||||
try:
|
||||
body = request.body
|
||||
if isinstance(body, bytes):
|
||||
body = body.decode('utf-8')
|
||||
request_data = json.loads(body) if body and body.strip() else {}
|
||||
except json.JSONDecodeError as e:
|
||||
final_dic = {'status': 0, 'error_message': 'Invalid JSON in request: %s' % str(e)}
|
||||
return HttpResponse(json.dumps(final_dic), content_type='application/json', status=400)
|
||||
except Exception as e:
|
||||
final_dic = {'status': 0, 'error_message': 'Error parsing request: %s' % str(e)}
|
||||
return HttpResponse(json.dumps(final_dic), content_type='application/json', status=400)
|
||||
return fm.deleteBannedIP(userID, request_data)
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
final_dic = {'status': 0, 'error_message': 'Session expired. Please refresh the page and try again.'}
|
||||
return HttpResponse(json.dumps(final_dic), content_type='application/json', status=403)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as _log
|
||||
error_trace = traceback.format_exc()
|
||||
_log.writeToFile('Error in deleteBannedIP view: %s\n%s' % (str(e), error_trace))
|
||||
final_dic = {'status': 0, 'error_message': 'Server error: %s' % str(e)}
|
||||
return HttpResponse(json.dumps(final_dic), content_type='application/json', status=500)
|
||||
|
||||
|
||||
def exportFirewallRules(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
fm = FirewallManager()
|
||||
fm = FirewallManager(request)
|
||||
return fm.exportFirewallRules(userID)
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user