Merge pull request #1501 from master3395/v2.5.5-dev

V2.5.5 dev
This commit is contained in:
Usman Nasir 2025-09-14 22:14:52 +05:00 committed by GitHub
commit 7bd67d3df2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 1318 additions and 167 deletions

36
.env.template Normal file
View File

@ -0,0 +1,36 @@
# CyberPanel Environment Configuration Template
# Copy this file to .env and update with your actual values
# NEVER commit .env to version control!
# Django Configuration
SECRET_KEY=your_very_long_random_secret_key_here_minimum_50_characters
DEBUG=False
ALLOWED_HOSTS=localhost,127.0.0.1,yourdomain.com
# Database Configuration - CyberPanel Database
DB_NAME=cyberpanel
DB_USER=cyberpanel
DB_PASSWORD=your_secure_cyberpanel_db_password_here
DB_HOST=localhost
DB_PORT=3306
# Root Database Configuration - MySQL Root Access
ROOT_DB_NAME=mysql
ROOT_DB_USER=root
ROOT_DB_PASSWORD=your_secure_mysql_root_password_here
ROOT_DB_HOST=localhost
ROOT_DB_PORT=3306
# Security Settings
SECURE_SSL_REDIRECT=False
SECURE_HSTS_SECONDS=0
SECURE_HSTS_INCLUDE_SUBDOMAINS=False
SECURE_HSTS_PRELOAD=False
SESSION_COOKIE_SECURE=False
CSRF_COOKIE_SECURE=False
# File Upload Settings
DATA_UPLOAD_MAX_MEMORY_SIZE=2147483648
# Logging Configuration
LOG_LEVEL=INFO

125
.gitignore vendored
View File

@ -1,7 +1,120 @@
# CyberPanel .gitignore
# Environment variables (CRITICAL - Contains sensitive data)
.env
.env.backup
.env.local
.env.production
.env.staging
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Virtual environments
venv/
env/
ENV/
env.bak/
venv.bak/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store .DS_Store
.AppleDouble .DS_Store?
.LSOverride ._*
*.pyc .Spotlight-V100
.idea .Trashes
venv ehthumbs.db
/.venv/ Thumbs.db
# Logs
*.log
logs/
log/
# Database
*.db
*.sqlite3
# Temporary files
tmp/
temp/
*.tmp
# Backup files
*.bak
*.backup
# Node modules (if any frontend build tools are used)
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Coverage reports
htmlcov/
.coverage
.coverage.*
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# CyberPanel specific
/usr/local/CyberCP/
/etc/cyberpanel/
cyberpanel_password.txt
mysql_password.txt

249
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,249 @@
# Contributing to CyberPanel
Thank you for your interest in contributing to CyberPanel! This document provides guidelines and information for contributors.
## 🌿 Branch Structure
CyberPanel uses a structured branching strategy to manage development and releases:
### **Branch Types**
1. **`stable`** - Production-ready stable branch
2. **`vX.X.X`** - Version-specific stable branch (e.g., `v2.4.3`)
3. **`vX.X.X-dev`** - Development branch for specific version (e.g., `v2.4.3-dev`)
## 🔄 Development Lifecycle
### **Development Process**
1. **Default Branch**: The latest `vX.X.X-dev` branch serves as the default (master) branch
2. **Contributions**: All contributors must push to the latest `vX.X.X-dev` branch
3. **Stability Check**: Once development is complete and believed to be stable, a new `vX.X.X` stable branch is created from the dev branch
4. **Merge Process**: The `vX.X.X` stable branch is then merged into the main `stable` branch
5. **New Development**: A new `vX.X.X-dev` branch is created and becomes the default branch
6. **Cleanup**: Old dev branches are deleted to save space
### **Important Rules**
- ✅ **DO**: Create pull requests only for the latest dev branch
- ❌ **DON'T**: Create pull requests for any other branches (stable, old dev branches, etc.)
- 🔄 **Development**: All development happens only in the latest dev branch
- 🗑️ **Cleanup**: Old dev branches are deleted after merging to stable
## 🚀 Getting Started
### **Prerequisites**
- Python 3.6+ (see supported versions in README.md)
- Django framework knowledge
- Basic understanding of web hosting control panels
- Git version control
### **Setup Development Environment**
1. **Fork the Repository**
```bash
# Fork the repository on GitHub, then clone your fork
git clone https://github.com/YOUR_USERNAME/cyberpanel.git
cd cyberpanel
```
2. **Add Upstream Remote**
```bash
git remote add upstream https://github.com/usmannasir/cyberpanel.git
```
3. **Create Development Branch**
```bash
# Checkout the latest dev branch
git checkout vX.X.X-dev
git pull upstream vX.X.X-dev
```
4. **Install Dependencies**
```bash
# Install Python dependencies
pip install -r requirements.txt
```
## 📝 Making Contributions
### **Code Style Guidelines**
- Follow PEP 8 for Python code
- Use meaningful variable and function names
- Add comments for complex logic
- Write comprehensive docstrings for functions and classes
- Ensure all code is properly tested
### **Commit Message Format**
Use clear, descriptive commit messages:
```
type(scope): brief description
Detailed description of changes made.
- List specific changes
- Explain why changes were made
- Reference any related issues
Fixes #123
```
**Types**: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`
### **Pull Request Process**
1. **Create Feature Branch**
```bash
git checkout -b feature/your-feature-name
```
2. **Make Changes**
- Write your code
- Add tests if applicable
- Update documentation if needed
3. **Test Your Changes**
```bash
# Run tests
python manage.py test
# Check for linting issues
flake8 .
```
4. **Commit Changes**
```bash
git add .
git commit -m "feat(module): add new feature"
```
5. **Push and Create PR**
```bash
git push origin feature/your-feature-name
```
Then create a pull request on GitHub targeting the latest dev branch.
## 🧪 Testing
### **Test Requirements**
- All new features must include tests
- Bug fixes must include regression tests
- Ensure all existing tests pass
- Maintain or improve test coverage
### **Running Tests**
```bash
# Run all tests
python manage.py test
# Run specific test module
python manage.py test module_name.tests
# Run with coverage
coverage run --source='.' manage.py test
coverage report
```
## 📋 Issue Reporting
### **Before Reporting**
- Check existing issues to avoid duplicates
- Ensure you're using the latest version
- Verify the issue exists in the latest dev branch
### **Issue Template**
When creating an issue, include:
- **OS and Version**: Your operating system and CyberPanel version
- **Steps to Reproduce**: Clear, numbered steps
- **Expected Behavior**: What should happen
- **Actual Behavior**: What actually happens
- **Screenshots**: If applicable
- **Logs**: Relevant error logs from `/usr/local/lscp/logs/`
## 🔒 Security
### **Security Issues**
For security-related issues:
- **DO NOT** create public issues
- Email security concerns to: security@cyberpanel.net
- Include detailed information about the vulnerability
- Allow time for the team to address before public disclosure
## 📚 Documentation
### **Documentation Guidelines**
- Update relevant documentation when adding features
- Use clear, concise language
- Include code examples where helpful
- Follow the existing documentation style
- Update README.md if adding new features or changing installation process
## 🤝 Code Review Process
### **Review Criteria**
- Code quality and style
- Test coverage
- Documentation updates
- Security considerations
- Performance impact
- Backward compatibility
### **Review Timeline**
- Initial review: Within 48 hours
- Follow-up reviews: Within 24 hours
- Merge decision: Within 1 week (for approved PRs)
## 🏷️ Release Process
### **Version Numbering**
CyberPanel follows semantic versioning (MAJOR.MINOR.PATCH):
- **MAJOR**: Breaking changes
- **MINOR**: New features (backward compatible)
- **PATCH**: Bug fixes (backward compatible)
### **Release Schedule**
- **Stable Releases**: Monthly or as needed
- **Hotfixes**: As critical issues arise
- **Development**: Continuous integration
## 💬 Community
### **Getting Help**
- 📚 [Documentation](https://cyberpanel.net/KnowledgeBase/)
- 💬 [Discord](https://discord.gg/g8k8Db3)
- 📢 [Forums](https://community.cyberpanel.net)
- 📵 [Facebook Group](https://www.facebook.com/groups/cyberpanel)
### **Contributing Guidelines**
- Be respectful and constructive
- Help others learn and grow
- Follow the code of conduct
- Ask questions when unsure
## 📄 License
By contributing to CyberPanel, you agree that your contributions will be licensed under the same license as the project (GPL-3.0).
---
**Thank you for contributing to CyberPanel!** 🎉
Your contributions help make web hosting management easier for thousands of users worldwide.

View File

@ -13,6 +13,14 @@ https://docs.djangoproject.com/en/1.11/ref/settings/
import os import os
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
# Load environment variables from .env file
try:
from dotenv import load_dotenv
load_dotenv()
except ImportError:
# dotenv not available, continue without it
pass
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@ -20,12 +28,13 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'xr%j*p!*$0d%(-(e%@-*hyoz4$f%y77coq0u)6pwmjg4)q&19f' SECRET_KEY = os.getenv('SECRET_KEY', 'xr%j*p!*$0d%(-(e%@-*hyoz4$f%y77coq0u)6pwmjg4)q&19f')
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False DEBUG = os.getenv('DEBUG', 'False').lower() == 'true'
ALLOWED_HOSTS = ['*'] # Allow configuration via environment variable, fallback to wildcard for backward compatibility
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', '*').split(',')
# Application definition # Application definition
@ -96,6 +105,7 @@ TEMPLATES = [
'django.contrib.auth.context_processors.auth', 'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages', 'django.contrib.messages.context_processors.messages',
'baseTemplate.context_processors.version_context', 'baseTemplate.context_processors.version_context',
'baseTemplate.context_processors.cosmetic_context',
], ],
}, },
}, },
@ -110,19 +120,19 @@ WSGI_APPLICATION = 'CyberCP.wsgi.application'
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.mysql', 'ENGINE': 'django.db.backends.mysql',
'NAME': 'cyberpanel', 'NAME': os.getenv('DB_NAME', 'cyberpanel'),
'USER': 'cyberpanel', 'USER': os.getenv('DB_USER', 'cyberpanel'),
'PASSWORD': 'SLTUIUxqhulwsh', 'PASSWORD': os.getenv('DB_PASSWORD', 'SLTUIUxqhulwsh'),
'HOST': 'localhost', 'HOST': os.getenv('DB_HOST', 'localhost'),
'PORT':'' 'PORT': os.getenv('DB_PORT', '3306'),
}, },
'rootdb': { 'rootdb': {
'ENGINE': 'django.db.backends.mysql', 'ENGINE': 'django.db.backends.mysql',
'NAME': 'mysql', 'NAME': os.getenv('ROOT_DB_NAME', 'mysql'),
'USER': 'root', 'USER': os.getenv('ROOT_DB_USER', 'root'),
'PASSWORD': 'SLTUIUxqhulwsh', 'PASSWORD': os.getenv('ROOT_DB_PASSWORD', 'SLTUIUxqhulwsh'),
'HOST': 'localhost', 'HOST': os.getenv('ROOT_DB_HOST', 'localhost'),
'PORT': '', 'PORT': os.getenv('ROOT_DB_PORT', '3306'),
}, },
} }
DATABASE_ROUTERS = ['backup.backupRouter.backupRouter'] DATABASE_ROUTERS = ['backup.backupRouter.backupRouter']

View File

@ -145,6 +145,7 @@ sh <(curl https://raw.githubusercontent.com/usmannasir/cyberpanel/stable/preUpgr
- 🎓 [Docs (Old)](https://community.cyberpanel.net/docs) - 🎓 [Docs (Old)](https://community.cyberpanel.net/docs)
- 📖 [Additional Guides](guides/INDEX.md) - Detailed guides for Docker, AI Scanner, Mautic, and more - 📖 [Additional Guides](guides/INDEX.md) - Detailed guides for Docker, AI Scanner, Mautic, and more
- 📚 [Local Documentation](guides/) - All guides available in this repository - 📚 [Local Documentation](guides/) - All guides available in this repository
- 🤝 [Contributing Guide](CONTRIBUTING.md) - How to contribute to CyberPanel development
- ✅ [Changelog](https://community.cyberpanel.net/t/change-logs/161) - ✅ [Changelog](https://community.cyberpanel.net/t/change-logs/161)
- 💬 [Forums](https://community.cyberpanel.net) - 💬 [Forums](https://community.cyberpanel.net)
- 📢 [Discord](https://discord.gg/g8k8Db3) - 📢 [Discord](https://discord.gg/g8k8Db3)

193
SECURITY_INSTALLATION.md Normal file
View File

@ -0,0 +1,193 @@
# CyberPanel Secure Installation Guide
## Overview
This document describes the secure installation process for CyberPanel that eliminates hardcoded passwords and implements environment-based configuration.
## Security Improvements
### ✅ **Fixed Security Vulnerabilities**
1. **Hardcoded Database Passwords** - Now generated securely during installation
2. **Hardcoded Django Secret Key** - Now generated using cryptographically secure random generation
3. **Environment Variables** - All sensitive configuration moved to `.env` file
4. **File Permissions** - `.env` file set to 600 (owner read/write only)
### 🔐 **Security Features**
- **Cryptographically Secure Passwords**: Uses Python's `secrets` module for password generation
- **Environment-based Configuration**: Sensitive data stored in `.env` file, not in code
- **Secure File Permissions**: Environment files protected with 600 permissions
- **Credential Backup**: Automatic backup of credentials for recovery
- **Fallback Security**: Maintains backward compatibility with fallback method
## Installation Process
### 1. **Automatic Secure Installation**
The installation script now automatically:
1. Generates secure random passwords for:
- MySQL root user
- CyberPanel database user
- Django secret key
2. Creates `.env` file with secure configuration:
```bash
# Generated during installation
SECRET_KEY=your_64_character_secure_key
DB_PASSWORD=your_24_character_secure_password
ROOT_DB_PASSWORD=your_24_character_secure_password
```
3. Creates `.env.backup` file for credential recovery
4. Sets secure file permissions (600) on all environment files
### 2. **Manual Installation** (if needed)
If you need to manually generate environment configuration:
```bash
cd /usr/local/CyberCP
python install/env_generator.py /usr/local/CyberCP
```
## File Structure
```
/usr/local/CyberCP/
├── .env # Main environment configuration (600 permissions)
├── .env.backup # Credential backup (600 permissions)
├── .env.template # Template for manual configuration
├── .gitignore # Prevents .env files from being committed
└── CyberCP/
└── settings.py # Updated to use environment variables
```
## Security Best Practices
### ✅ **Do's**
- Keep `.env` and `.env.backup` files secure
- Record credentials from `.env.backup` and delete the file after installation
- Use strong, unique passwords for production deployments
- Regularly rotate database passwords
- Monitor access to environment files
### ❌ **Don'ts**
- Never commit `.env` files to version control
- Don't share `.env` files via insecure channels
- Don't use default passwords in production
- Don't leave `.env.backup` files on the system after recording credentials
## Recovery
### **Lost Credentials**
If you lose your database credentials:
1. Check if `.env.backup` file exists:
```bash
sudo cat /usr/local/CyberCP/.env.backup
```
2. If backup doesn't exist, you'll need to reset MySQL passwords using MySQL recovery procedures
### **Regenerate Environment**
To regenerate environment configuration:
```bash
cd /usr/local/CyberCP
sudo python install/env_generator.py /usr/local/CyberCP
```
## Configuration Options
### **Environment Variables**
| Variable | Description | Default |
|----------|-------------|---------|
| `SECRET_KEY` | Django secret key | Generated (64 chars) |
| `DB_PASSWORD` | CyberPanel DB password | Generated (24 chars) |
| `ROOT_DB_PASSWORD` | MySQL root password | Generated (24 chars) |
| `DEBUG` | Debug mode | False |
| `ALLOWED_HOSTS` | Allowed hosts | localhost,127.0.0.1,hostname |
### **Custom Configuration**
To use custom passwords during installation:
```bash
python install/env_generator.py /usr/local/CyberCP "your_root_password" "your_db_password"
```
## Troubleshooting
### **Installation Fails**
If the new secure installation fails:
1. Check installation logs for error messages
2. The system will automatically fallback to the original installation method
3. Verify Python dependencies are installed:
```bash
pip install python-dotenv
```
### **Environment Loading Issues**
If Django can't load environment variables:
1. Ensure `.env` file exists and has correct permissions:
```bash
ls -la /usr/local/CyberCP/.env
# Should show: -rw------- 1 root root
```
2. Install python-dotenv if missing:
```bash
pip install python-dotenv
```
## Migration from Old Installation
### **Existing Installations**
For existing CyberPanel installations with hardcoded passwords:
1. **Backup current configuration**:
```bash
cp /usr/local/CyberCP/CyberCP/settings.py /usr/local/CyberCP/CyberCP/settings.py.backup
```
2. **Generate new environment configuration**:
```bash
cd /usr/local/CyberCP
python install/env_generator.py /usr/local/CyberCP
```
3. **Update settings.py** (already done in new installations):
- The settings.py file now supports environment variables
- It will fallback to hardcoded values if .env is not available
4. **Test the configuration**:
```bash
cd /usr/local/CyberCP
python manage.py check
```
## Support
For issues with the secure installation:
1. Check the installation logs
2. Verify file permissions
3. Ensure all dependencies are installed
4. Review the fallback installation method if needed
---
**Security Notice**: This installation method significantly improves security by eliminating hardcoded credentials. Always ensure proper file permissions and secure handling of environment files.

View File

@ -2074,6 +2074,8 @@ app.controller('scheduleBackup', function ($scope, $http, $window) {
$scope.allSites = response.data.allSites; $scope.allSites = response.data.allSites;
$scope.lastRun = response.data.lastRun; $scope.lastRun = response.data.lastRun;
$scope.currentStatus = response.data.currentStatus; $scope.currentStatus = response.data.currentStatus;
$scope.backupFrequency = response.data.currently;
$scope.backupRetention = response.data.retention;
} else { } else {
new PNotify({ new PNotify({

View File

@ -8,3 +8,19 @@ def version_context(request):
'CYBERPANEL_BUILD': BUILD, 'CYBERPANEL_BUILD': BUILD,
'CYBERPANEL_FULL_VERSION': f"{VERSION}.{BUILD}" 'CYBERPANEL_FULL_VERSION': f"{VERSION}.{BUILD}"
} }
def cosmetic_context(request):
"""Add cosmetic data (custom CSS) to all templates"""
try:
from .models import CyberPanelCosmetic
cosmetic = CyberPanelCosmetic.objects.get(pk=1)
return {
'cosmetic': cosmetic
}
except:
from .models import CyberPanelCosmetic
cosmetic = CyberPanelCosmetic()
cosmetic.save()
return {
'cosmetic': cosmetic
}

View File

@ -15,6 +15,11 @@
<link rel="stylesheet" type="text/css" href="{% static 'baseTemplate/assets/bootstrap/css/bootstrap.min.css' %}?v={{ CP_VERSION }}"> <link rel="stylesheet" type="text/css" href="{% static 'baseTemplate/assets/bootstrap/css/bootstrap.min.css' %}?v={{ CP_VERSION }}">
<link rel="stylesheet" type="text/css" href="{% static 'baseTemplate/bootstrap-toggle.min.css' %}?v={{ CP_VERSION }}"> <link rel="stylesheet" type="text/css" href="{% static 'baseTemplate/bootstrap-toggle.min.css' %}?v={{ CP_VERSION }}">
<!-- Custom CSS -->
<style>
{{ cosmetic.MainDashboardCSS | safe }}
</style>
<!-- Core Scripts --> <!-- Core Scripts -->
<script src="{% static 'baseTemplate/angularjs.1.6.5.js' %}?v={{ CP_VERSION }}"></script> <script src="{% static 'baseTemplate/angularjs.1.6.5.js' %}?v={{ CP_VERSION }}"></script>
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script> <script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>

View File

@ -1,14 +0,0 @@
Branches
1.Stable-> Stable branch
2.vX.X.X-> vX.X.X Stable branch
3.vX.X.X-dev-> v.X.X.X Dev branch
Development Lifecycle
vX.X.X-dev will be default(master) branch. All contributors must push to latest vX.X.X-dev branch. Once development
is complete(believed to be stable) new vX.X.X Stable branch will be created from Dev branch. Then vX.X.X Stable will
be merged into Stable branch. After that a new vX.X.X-dev branch will be created and it will be default(master)
branch. Old dev branch will be deleted at this stage(to save space) and no development will happen on old stable or
dev(if not deleted) branch. All development will only take place in latest dev branch. You must not create pull
request for any other branches other than latest dev branch.

177
install/env_generator.py Normal file
View File

@ -0,0 +1,177 @@
#!/usr/bin/env python3
"""
CyberPanel Environment Configuration Generator
Generates secure .env file with random passwords during installation
"""
import os
import sys
import secrets
import string
from pathlib import Path
def generate_secure_password(length=24):
"""
Generate a cryptographically secure password
Args:
length: Length of the password to generate (default 24)
Returns:
str: Random password containing uppercase, lowercase, digits and safe special chars
"""
# Use safe characters that don't require escaping in most contexts
safe_chars = string.ascii_letters + string.digits + '!@#$%^&*'
return ''.join(secrets.choice(safe_chars) for _ in range(length))
def generate_secret_key(length=64):
"""
Generate a cryptographically secure Django secret key
Args:
length: Length of the secret key to generate (default 64)
Returns:
str: Random secret key
"""
chars = string.ascii_letters + string.digits + '!@#$%^&*(-_=+)'
return ''.join(secrets.choice(chars) for _ in range(length))
def create_env_file(cyberpanel_path, mysql_root_password=None, cyberpanel_db_password=None):
"""
Create .env file with generated secure credentials
Args:
cyberpanel_path: Path to CyberPanel installation directory
mysql_root_password: Optional MySQL root password (will generate if None)
cyberpanel_db_password: Optional CyberPanel DB password (will generate if None)
"""
# Generate secure passwords if not provided
if not mysql_root_password:
mysql_root_password = generate_secure_password(24)
if not cyberpanel_db_password:
cyberpanel_db_password = generate_secure_password(24)
secret_key = generate_secret_key(64)
# Get hostname for ALLOWED_HOSTS
import socket
try:
hostname = socket.gethostname()
local_ip = socket.gethostbyname(hostname)
except:
hostname = 'localhost'
local_ip = '127.0.0.1'
# Create .env content
env_content = f"""# CyberPanel Environment Configuration
# Generated automatically during installation - DO NOT EDIT MANUALLY
# Generated on: {__import__('datetime').datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
# Django Configuration
SECRET_KEY={secret_key}
DEBUG=False
ALLOWED_HOSTS=localhost,127.0.0.1,{hostname},{local_ip}
# Database Configuration - CyberPanel Database
DB_NAME=cyberpanel
DB_USER=cyberpanel
DB_PASSWORD={cyberpanel_db_password}
DB_HOST=localhost
DB_PORT=3306
# Root Database Configuration - MySQL Root Access
ROOT_DB_NAME=mysql
ROOT_DB_USER=root
ROOT_DB_PASSWORD={mysql_root_password}
ROOT_DB_HOST=localhost
ROOT_DB_PORT=3306
# Security Settings
SECURE_SSL_REDIRECT=False
SECURE_HSTS_SECONDS=0
SECURE_HSTS_INCLUDE_SUBDOMAINS=False
SECURE_HSTS_PRELOAD=False
SESSION_COOKIE_SECURE=False
CSRF_COOKIE_SECURE=False
# File Upload Settings
DATA_UPLOAD_MAX_MEMORY_SIZE=2147483648
# Logging Configuration
LOG_LEVEL=INFO
"""
# Write .env file
env_file_path = os.path.join(cyberpanel_path, '.env')
with open(env_file_path, 'w') as f:
f.write(env_content)
# Set secure permissions (owner read/write only)
os.chmod(env_file_path, 0o600)
print(f"✓ Generated secure .env file at: {env_file_path}")
print(f"✓ MySQL Root Password: {mysql_root_password}")
print(f"✓ CyberPanel DB Password: {cyberpanel_db_password}")
print(f"✓ Django Secret Key: {secret_key[:20]}...")
return {
'mysql_root_password': mysql_root_password,
'cyberpanel_db_password': cyberpanel_db_password,
'secret_key': secret_key
}
def create_env_backup(cyberpanel_path, credentials):
"""
Create a secure backup of credentials for recovery purposes
Args:
cyberpanel_path: Path to CyberPanel installation directory
credentials: Dictionary containing generated credentials
"""
backup_content = f"""# CyberPanel Credentials Backup
# Generated: {__import__('datetime').datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
#
# IMPORTANT: Store this file securely and delete it after recording credentials
# These are your database passwords and should be kept confidential
MySQL Root Password: {credentials['mysql_root_password']}
CyberPanel Database Password: {credentials['cyberpanel_db_password']}
Django Secret Key: {credentials['secret_key']}
# To restore these credentials, copy them to your .env file
"""
backup_file_path = os.path.join(cyberpanel_path, '.env.backup')
with open(backup_file_path, 'w') as f:
f.write(backup_content)
# Set secure permissions (owner read/write only)
os.chmod(backup_file_path, 0o600)
print(f"✓ Created credentials backup at: {backup_file_path}")
print("⚠️ IMPORTANT: Record these credentials and delete the backup file for security")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python env_generator.py <cyberpanel_path> [mysql_root_password] [cyberpanel_db_password]")
sys.exit(1)
cyberpanel_path = sys.argv[1]
mysql_root_password = sys.argv[2] if len(sys.argv) > 2 else None
cyberpanel_db_password = sys.argv[3] if len(sys.argv) > 3 else None
if not os.path.exists(cyberpanel_path):
print(f"Error: CyberPanel path does not exist: {cyberpanel_path}")
sys.exit(1)
try:
credentials = create_env_file(cyberpanel_path, mysql_root_password, cyberpanel_db_password)
create_env_backup(cyberpanel_path, credentials)
print("\n✓ Environment configuration generated successfully!")
print("✓ Remember to delete .env.backup after recording credentials")
except Exception as e:
print(f"Error generating environment configuration: {e}")
sys.exit(1)

View File

@ -504,6 +504,73 @@ class preFlightsChecks:
self.stdOut("Install psmisc") self.stdOut("Install psmisc")
self.install_package("psmisc") self.install_package("psmisc")
def generate_secure_env_file(self, mysql_root_password, cyberpanel_db_password):
"""
Generate secure .env file with random passwords during installation
"""
try:
import sys
import socket
# Import the environment generator
sys.path.append(os.path.join(self.cyberPanelPath, 'install'))
from env_generator import create_env_file, create_env_backup
# Generate secure credentials
credentials = create_env_file(
self.cyberPanelPath,
mysql_root_password,
cyberpanel_db_password
)
# Create backup for recovery
create_env_backup(self.cyberPanelPath, credentials)
logging.InstallLog.writeToFile("✓ Secure .env file generated successfully")
logging.InstallLog.writeToFile("✓ Credentials backup created for recovery")
return credentials
except Exception as e:
logging.InstallLog.writeToFile(f"[ERROR] Failed to generate secure environment file: {str(e)}")
# Fallback to original method if environment generation fails
self.fallback_settings_update(mysql_root_password, cyberpanel_db_password)
def fallback_settings_update(self, mysqlPassword, password):
"""
Fallback method to update settings.py directly if environment generation fails
"""
logging.InstallLog.writeToFile("Using fallback method for settings.py update")
path = self.cyberPanelPath + "/CyberCP/settings.py"
data = open(path, "r").readlines()
writeDataToFile = open(path, "w")
counter = 0
for items in data:
if items.find('SECRET_KEY') > -1:
SK = "SECRET_KEY = '%s'\n" % (generate_pass(50))
writeDataToFile.writelines(SK)
continue
if items.find("'PASSWORD':") > -1:
if counter == 0:
writeDataToFile.writelines(" 'PASSWORD': '" + mysqlPassword + "'," + "\n")
counter = counter + 1
else:
writeDataToFile.writelines(" 'PASSWORD': '" + password + "'," + "\n")
elif items.find('127.0.0.1') > -1:
writeDataToFile.writelines(" 'HOST': 'localhost',\n")
elif items.find("'PORT':'3307'") > -1:
writeDataToFile.writelines(" 'PORT': '',\n")
else:
writeDataToFile.writelines(items)
if self.distro == ubuntu:
os.fchmod(writeDataToFile.fileno(), stat.S_IRUSR | stat.S_IWUSR)
writeDataToFile.close()
def download_install_CyberPanel(self, mysqlPassword, mysql): def download_install_CyberPanel(self, mysqlPassword, mysql):
## ##
@ -549,50 +616,12 @@ password="%s"
logging.InstallLog.writeToFile("Updating /root/.my.cnf!") logging.InstallLog.writeToFile("Updating /root/.my.cnf!")
logging.InstallLog.writeToFile("Updating settings.py!") logging.InstallLog.writeToFile("Generating secure environment configuration!")
path = self.cyberPanelPath + "/CyberCP/settings.py" # Generate secure environment file instead of hardcoding passwords
self.generate_secure_env_file(mysqlPassword, password)
data = open(path, "r").readlines() logging.InstallLog.writeToFile("Environment configuration generated successfully!")
writeDataToFile = open(path, "w")
counter = 0
for items in data:
if items.find('SECRET_KEY') > -1:
SK = "SECRET_KEY = '%s'\n" % (generate_pass(50))
writeDataToFile.writelines(SK)
continue
if mysql == 'Two':
if items.find("'PASSWORD':") > -1:
if counter == 0:
writeDataToFile.writelines(" 'PASSWORD': '" + mysqlPassword + "'," + "\n")
counter = counter + 1
else:
writeDataToFile.writelines(" 'PASSWORD': '" + password + "'," + "\n")
else:
writeDataToFile.writelines(items)
else:
if items.find("'PASSWORD':") > -1:
if counter == 0:
writeDataToFile.writelines(" 'PASSWORD': '" + mysqlPassword + "'," + "\n")
counter = counter + 1
else:
writeDataToFile.writelines(" 'PASSWORD': '" + password + "'," + "\n")
elif items.find('127.0.0.1') > -1:
writeDataToFile.writelines(" 'HOST': 'localhost',\n")
elif items.find("'PORT':'3307'") > -1:
writeDataToFile.writelines(" 'PORT': '',\n")
else:
writeDataToFile.writelines(items)
if self.distro == ubuntu:
os.fchmod(writeDataToFile.fileno(), stat.S_IRUSR | stat.S_IWUSR)
writeDataToFile.close()
if self.remotemysql == 'ON': if self.remotemysql == 'ON':
command = "sed -i 's|localhost|%s|g' %s" % (self.mysqlhost, path) command = "sed -i 's|localhost|%s|g' %s" % (self.mysqlhost, path)

View File

@ -242,6 +242,16 @@
margin-right: 0.5rem; margin-right: 0.5rem;
} }
.password-hint {
color: var(--text-secondary, #64748b);
margin-top: 0.5rem;
display: block;
}
.action-section {
margin-top: 2rem;
}
@keyframes spin { @keyframes spin {
to { transform: rotate(360deg); } to { transform: rotate(360deg); }
} }
@ -324,7 +334,7 @@
<div class="col-md-8"> <div class="col-md-8">
<div class="form-group"> <div class="form-group">
<label class="form-label">{% trans "Select Website" %}</label> <label class="form-label">{% trans "Select Website" %}</label>
<select ng-change="showEmailDetails()" ng-model="emailDomain" class="form-control"> <select ng-change="showEmailDetails()" ng-model="emailDomain" class="form-control" aria-label="{% trans 'Select Website' %}">
<option value="">{% trans "Choose a website..." %}</option> <option value="">{% trans "Choose a website..." %}</option>
{% for items in websiteList %} {% for items in websiteList %}
<option value="{{ items }}">{{ items }}</option> <option value="{{ items }}">{{ items }}</option>
@ -345,7 +355,7 @@
<div class="col-md-8"> <div class="col-md-8">
<div class="form-group"> <div class="form-group">
<label class="form-label">{% trans "Select Email Account" %}</label> <label class="form-label">{% trans "Select Email Account" %}</label>
<select ng-model="selectedEmail" class="form-control"> <select ng-model="selectedEmail" class="form-control" aria-label="{% trans 'Select Email Account' %}">
<option value="">{% trans "Choose an email account..." %}</option> <option value="">{% trans "Choose an email account..." %}</option>
<option ng-repeat="email in emails track by $index" value="{$ email.email $}">{$ email.email $}</option> <option ng-repeat="email in emails track by $index" value="{$ email.email $}">{$ email.email $}</option>
</select> </select>
@ -374,19 +384,19 @@
<label class="form-label">{% trans "Generated Password" %}</label> <label class="form-label">{% trans "Generated Password" %}</label>
<div class="input-group"> <div class="input-group">
<input type="text" name="email" class="form-control" <input type="text" name="email" class="form-control"
ng-model="emailPassword" readonly> ng-model="emailPassword" readonly aria-label="{% trans 'Generated Password' %}">
<button type="button" ng-click="usePassword()" class="btn-secondary"> <button type="button" ng-click="usePassword()" class="btn-secondary">
<i class="fas fa-check"></i> <i class="fas fa-check"></i>
{% trans "Use This Password" %} {% trans "Use This Password" %}
</button> </button>
</div> </div>
<small style="color: var(--text-secondary, #64748b); margin-top: 0.5rem; display: block;"> <small class="password-hint">
<i class="fas fa-info-circle"></i> <i class="fas fa-info-circle"></i>
{% trans "Make sure to save this password in a secure location" %} {% trans "Make sure to save this password in a secure location" %}
</small> </small>
</div> </div>
<div style="margin-top: 2rem;"> <div class="action-section">
<button type="button" ng-click="changePassword()" class="btn-primary"> <button type="button" ng-click="changePassword()" class="btn-primary">
<i class="fas fa-save"></i> <i class="fas fa-save"></i>
{% trans "Change Password" %} {% trans "Change Password" %}

View File

@ -135,6 +135,7 @@
background: var(--bg-hover, #f8f9ff); background: var(--bg-hover, #f8f9ff);
border: 1px solid var(--border-color, #e8e9ff); border: 1px solid var(--border-color, #e8e9ff);
border-radius: 8px; border-radius: 8px;
min-width: -webkit-fill-available;
min-width: fit-content; min-width: fit-content;
} }
@ -304,6 +305,16 @@
width: 100%; width: 100%;
background: var(--success-color, #10b981); background: var(--success-color, #10b981);
} }
.password-hint {
color: var(--text-secondary, #64748b);
margin-top: 0.5rem;
display: block;
}
.action-section {
margin-top: 2rem;
}
</style> </style>
<div class="modern-container" ng-controller="createEmailAccount"> <div class="modern-container" ng-controller="createEmailAccount">
@ -320,7 +331,7 @@
<h2 class="card-title"> <h2 class="card-title">
<i class="fas fa-user-plus"></i> <i class="fas fa-user-plus"></i>
{% trans "New Email Account" %} {% trans "New Email Account" %}
<a target="_blank" href="https://platform.cyberpersons.com/MailTester/MailTester?utm_source=from-cyberpanel-inside&utm_medium=from-cyberpanel-inside&utm_campaign=from-cyberpanel-inside&utm_id=from-cyberpanel-inside&utm_term=from-cyberpanel-inside" <a target="_blank" rel="noopener" href="https://platform.cyberpersons.com/MailTester/MailTester?utm_source=from-cyberpanel-inside&utm_medium=from-cyberpanel-inside&utm_campaign=from-cyberpanel-inside&utm_id=from-cyberpanel-inside&utm_term=from-cyberpanel-inside"
class="test-email-link"> class="test-email-link">
<i class="fas fa-vial"></i> <i class="fas fa-vial"></i>
{% trans "Test Email Delivery" %} {% trans "Test Email Delivery" %}
@ -346,7 +357,7 @@
<div class="col-md-8"> <div class="col-md-8">
<div class="form-group"> <div class="form-group">
<label class="form-label">{% trans "Select Website" %}</label> <label class="form-label">{% trans "Select Website" %}</label>
<select ng-change="showEmailDetails()" ng-model="emailDomain" class="form-control"> <select ng-change="showEmailDetails()" ng-model="emailDomain" class="form-control" aria-label="{% trans 'Select Website' %}">
<option value="">{% trans "Choose a website..." %}</option> <option value="">{% trans "Choose a website..." %}</option>
{% for items in websiteList %} {% for items in websiteList %}
<option value="{{ items }}">{{ items }}</option> <option value="{{ items }}">{{ items }}</option>
@ -397,19 +408,19 @@
<label class="form-label">{% trans "Generated Password" %}</label> <label class="form-label">{% trans "Generated Password" %}</label>
<div class="input-group"> <div class="input-group">
<input type="text" name="email" class="form-control" <input type="text" name="email" class="form-control"
ng-model="emailPassword" readonly> ng-model="emailPassword" readonly aria-label="{% trans 'Generated Password' %}">
<button type="button" ng-click="usePassword()" class="btn-secondary"> <button type="button" ng-click="usePassword()" class="btn-secondary">
<i class="fas fa-check"></i> <i class="fas fa-check"></i>
{% trans "Use This Password" %} {% trans "Use This Password" %}
</button> </button>
</div> </div>
<small style="color: var(--text-secondary, #64748b); margin-top: 0.5rem; display: block;"> <small class="password-hint">
<i class="fas fa-info-circle"></i> <i class="fas fa-info-circle"></i>
{% trans "Make sure to save this password in a secure location" %} {% trans "Make sure to save this password in a secure location" %}
</small> </small>
</div> </div>
<div style="margin-top: 2rem;"> <div class="action-section">
<button type="button" ng-click="createEmailAccount()" class="btn-primary"> <button type="button" ng-click="createEmailAccount()" class="btn-primary">
<i class="fas fa-plus-circle"></i> <i class="fas fa-plus-circle"></i>
{% trans "Create Email Account" %} {% trans "Create Email Account" %}

View File

@ -323,7 +323,7 @@
<div class="col-md-8"> <div class="col-md-8">
<div class="form-group"> <div class="form-group">
<label class="form-label">{% trans "Select Website" %}</label> <label class="form-label">{% trans "Select Website" %}</label>
<select ng-change="showEmailDetails()" ng-model="emailDomain" class="form-control"> <select ng-change="showEmailDetails()" ng-model="emailDomain" class="form-control" aria-label="{% trans 'Select Website' %}">
<option value="">{% trans "Choose a website..." %}</option> <option value="">{% trans "Choose a website..." %}</option>
{% for items in websiteList %} {% for items in websiteList %}
<option value="{{ items }}">{{ items }}</option> <option value="{{ items }}">{{ items }}</option>
@ -339,7 +339,7 @@
<div class="col-md-8"> <div class="col-md-8">
<div class="form-group"> <div class="form-group">
<label class="form-label">{% trans "Select Email Account" %}</label> <label class="form-label">{% trans "Select Email Account" %}</label>
<select ng-model="selectedEmail" class="form-control"> <select ng-model="selectedEmail" class="form-control" aria-label="{% trans 'Select Email Account' %}">
<option value="">{% trans "Choose an email account..." %}</option> <option value="">{% trans "Choose an email account..." %}</option>
<option ng-repeat="email in emails track by $index" value="{$ email.email $}">{$ email.email $}</option> <option ng-repeat="email in emails track by $index" value="{$ email.email $}">{$ email.email $}</option>
</select> </select>

View File

@ -307,6 +307,50 @@
margin-right: 0.5rem; margin-right: 0.5rem;
} }
.install-icon {
color: #5b5fcf;
}
.progress-title {
color: var(--text-primary, #1e293b);
font-size: 1.125rem;
font-weight: 600;
margin-bottom: 1rem;
}
.terminal-pre {
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
}
.table-domain {
width: 20%;
}
.table-private-key {
width: 40%;
}
.table-public-key {
width: 40%;
}
.domain-badge {
padding: 0.5rem 1rem;
background: var(--accent-light, #e0e7ff);
color: #5b5fcf;
border-radius: 6px;
font-weight: 600;
display: inline-block;
}
.key-hint {
color: var(--text-secondary, #64748b);
margin-top: 0.5rem;
display: block;
}
@keyframes spin { @keyframes spin {
to { transform: rotate(360deg); } to { transform: rotate(360deg); }
} }
@ -359,7 +403,7 @@
<h1 class="page-title"> <h1 class="page-title">
<i class="fas fa-shield-alt"></i> <i class="fas fa-shield-alt"></i>
{% trans "DKIM Manager" %} {% trans "DKIM Manager" %}
<a target="_blank" href="https://cyberpanel.net/KnowledgeBase/home/dkim-set-up-and-configurations/" class="docs-link"> <a target="_blank" rel="noopener" href="https://cyberpanel.net/KnowledgeBase/home/dkim-set-up-and-configurations/" class="docs-link">
<i class="fas fa-book"></i> <i class="fas fa-book"></i>
{% trans "DKIM Docs" %} {% trans "DKIM Docs" %}
</a> </a>
@ -378,7 +422,7 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="install-notice"> <div class="install-notice">
<i class="fas fa-exclamation-circle fa-3x mb-3" style="color: #5b5fcf;"></i> <i class="fas fa-exclamation-circle fa-3x mb-3 install-icon"></i>
<h3>{% trans "OpenDKIM is not installed" %}</h3> <h3>{% trans "OpenDKIM is not installed" %}</h3>
<p class="mb-3">{% trans "OpenDKIM is required to manage DKIM keys for your domains" %}</p> <p class="mb-3">{% trans "OpenDKIM is required to manage DKIM keys for your domains" %}</p>
<a href="" ng-click="installOpenDKIM()" class="install-link"> <a href="" ng-click="installOpenDKIM()" class="install-link">
@ -407,12 +451,12 @@
<!-- Installation Log --> <!-- Installation Log -->
<div ng-hide="openDKIMInstallBox"> <div ng-hide="openDKIMInstallBox">
<h3 style="color: var(--text-primary, #1e293b); font-size: 1.125rem; font-weight: 600; margin-bottom: 1rem;"> <h3 class="progress-title">
<i class="fas fa-terminal"></i> <i class="fas fa-terminal"></i>
{% trans "Installation Progress" %} {% trans "Installation Progress" %}
</h3> </h3>
<div class="terminal-box"> <div class="terminal-box">
<pre ng-bind="requestData" style="margin: 0; white-space: pre-wrap; word-wrap: break-word;"></pre> <pre ng-bind="requestData" class="terminal-pre"></pre>
</div> </div>
</div> </div>
</div> </div>
@ -438,7 +482,7 @@
<div class="col-md-8"> <div class="col-md-8">
<div class="form-group"> <div class="form-group">
<label class="form-label">{% trans "Select Website" %}</label> <label class="form-label">{% trans "Select Website" %}</label>
<select ng-change="fetchKeys()" ng-model="domainName" class="form-control"> <select ng-change="fetchKeys()" ng-model="domainName" class="form-control" aria-label="{% trans 'Select Website' %}">
<option value="">{% trans "Choose a website..." %}</option> <option value="">{% trans "Choose a website..." %}</option>
{% for items in websiteList %} {% for items in websiteList %}
<option value="{{ items }}">{{ items }}</option> <option value="{{ items }}">{{ items }}</option>
@ -465,29 +509,29 @@
<table class="keys-table"> <table class="keys-table">
<thead> <thead>
<tr> <tr>
<th style="width: 20%;">{% trans "Domain" %}</th> <th class="table-domain">{% trans "Domain" %}</th>
<th style="width: 40%;">{% trans "Private Key" %}</th> <th class="table-private-key">{% trans "Private Key" %}</th>
<th style="width: 40%;">{% trans "Public Key" %}</th> <th class="table-public-key">{% trans "Public Key" %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td> <td>
<span style="padding: 0.5rem 1rem; background: var(--accent-light, #e0e7ff); color: #5b5fcf; border-radius: 6px; font-weight: 600; display: inline-block;"> <span class="domain-badge">
<i class="fas fa-globe"></i> <i class="fas fa-globe"></i>
<span ng-bind="domainName"></span> <span ng-bind="domainName"></span>
</span> </span>
</td> </td>
<td> <td>
<textarea ng-bind="privateKey" rows="10" class="key-textarea" readonly></textarea> <textarea ng-bind="privateKey" rows="10" class="key-textarea" readonly aria-label="{% trans 'Private Key' %}"></textarea>
<small style="color: var(--text-secondary, #64748b); margin-top: 0.5rem; display: block;"> <small class="key-hint">
<i class="fas fa-lock"></i> <i class="fas fa-lock"></i>
{% trans "Keep this private key secure" %} {% trans "Keep this private key secure" %}
</small> </small>
</td> </td>
<td> <td>
<textarea ng-bind="publicKey" rows="10" class="key-textarea" readonly></textarea> <textarea ng-bind="publicKey" rows="10" class="key-textarea" readonly aria-label="{% trans 'Public Key' %}"></textarea>
<small style="color: var(--text-secondary, #64748b); margin-top: 0.5rem; display: block;"> <small class="key-hint">
<i class="fas fa-info-circle"></i> <i class="fas fa-info-circle"></i>
{% trans "Add this as a TXT record in your DNS" %} {% trans "Add this as a TXT record in your DNS" %}
</small> </small>

View File

@ -277,6 +277,40 @@
display: inline-block; display: inline-block;
} }
.warning-icon {
color: var(--warning-color, #ffa000);
}
.table-id {
width: 10%;
}
.table-source {
width: 35%;
}
.table-destination {
width: 40%;
}
.table-actions {
width: 15%;
text-align: center;
}
.email-icon {
color: var(--accent-color, #5b5fcf);
margin-right: 0.5rem;
}
.destination-text {
margin-left: 0.5rem;
}
.full-width-btn {
width: 100%;
}
@keyframes spin { @keyframes spin {
to { transform: rotate(360deg); } to { transform: rotate(360deg); }
} }
@ -329,7 +363,7 @@
<h1 class="page-title"> <h1 class="page-title">
<i class="fas fa-share-square"></i> <i class="fas fa-share-square"></i>
{% trans "Setup Email Forwarding" %} {% trans "Setup Email Forwarding" %}
<a target="_blank" href="https://cyberpanel.net/KnowledgeBase/home/email-forwarding/" class="docs-link"> <a target="_blank" rel="noopener" href="https://cyberpanel.net/KnowledgeBase/home/email-forwarding/" class="docs-link">
<i class="fas fa-book"></i> <i class="fas fa-book"></i>
{% trans "Forwarding Docs" %} {% trans "Forwarding Docs" %}
</a> </a>
@ -348,7 +382,7 @@
<div class="card-body"> <div class="card-body">
{% if not status %} {% if not status %}
<div class="disabled-notice"> <div class="disabled-notice">
<i class="fas fa-exclamation-triangle fa-3x mb-3" style="color: var(--warning-color, #ffa000);"></i> <i class="fas fa-exclamation-triangle fa-3x mb-3 warning-icon"></i>
<h3>{% trans "Postfix is disabled" %}</h3> <h3>{% trans "Postfix is disabled" %}</h3>
<p class="mb-3">{% trans "You need to enable Postfix to setup email forwarding" %}</p> <p class="mb-3">{% trans "You need to enable Postfix to setup email forwarding" %}</p>
<a href="{% url 'managePostfix' %}" class="btn-primary"> <a href="{% url 'managePostfix' %}" class="btn-primary">
@ -363,7 +397,7 @@
<div class="col-md-8"> <div class="col-md-8">
<div class="form-group"> <div class="form-group">
<label class="form-label">{% trans "Select Website" %}</label> <label class="form-label">{% trans "Select Website" %}</label>
<select ng-change="showEmailDetails()" ng-model="emailDomain" class="form-control"> <select ng-change="showEmailDetails()" ng-model="emailDomain" class="form-control" aria-label="{% trans 'Select Website' %}">
<option value="">{% trans "Choose a website..." %}</option> <option value="">{% trans "Choose a website..." %}</option>
{% for items in websiteList %} {% for items in websiteList %}
<option value="{{ items }}">{{ items }}</option> <option value="{{ items }}">{{ items }}</option>
@ -379,7 +413,7 @@
<div class="col-md-8"> <div class="col-md-8">
<div class="form-group"> <div class="form-group">
<label class="form-label">{% trans "Select Email Account" %}</label> <label class="form-label">{% trans "Select Email Account" %}</label>
<select ng-change="selectForwardingEmail()" ng-model="selectedEmail" class="form-control"> <select ng-change="selectForwardingEmail()" ng-model="selectedEmail" class="form-control" aria-label="{% trans 'Select Email Account' %}">
<option value="">{% trans "Choose an email account..." %}</option> <option value="">{% trans "Choose an email account..." %}</option>
<option ng-repeat="email in emails track by $index" value="{$ email.email $}">{$ email.email $}</option> <option ng-repeat="email in emails track by $index" value="{$ email.email $}">{$ email.email $}</option>
</select> </select>
@ -387,7 +421,7 @@
<div class="form-group"> <div class="form-group">
<label class="form-label">{% trans "Forwarding Options" %}</label> <label class="form-label">{% trans "Forwarding Options" %}</label>
<select ng-change="selectForwardingEmail()" ng-model="forwardingOption" class="form-control"> <select ng-change="selectForwardingEmail()" ng-model="forwardingOption" class="form-control" aria-label="{% trans 'Forwarding Options' %}">
<option value="Forward to email">{% trans "Forward to email" %}</option> <option value="Forward to email">{% trans "Forward to email" %}</option>
<option value="Pipe to program">{% trans "Pipe to program" %}</option> <option value="Pipe to program">{% trans "Pipe to program" %}</option>
</select> </select>
@ -402,21 +436,21 @@
<div class="col-md-4"> <div class="col-md-4">
<div class="form-group mb-3"> <div class="form-group mb-3">
<label class="form-label">{% trans "Source Email" %}</label> <label class="form-label">{% trans "Source Email" %}</label>
<input placeholder="{% trans 'Source' %}" type="email" class="form-control" ng-model="selectedEmail" readonly> <input placeholder="{% trans 'Source' %}" type="email" class="form-control" ng-model="selectedEmail" readonly aria-label="{% trans 'Source Email' %}">
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<div class="form-group mb-3"> <div class="form-group mb-3">
<label class="form-label">{% trans "Destination" %} <span ng-show="forwardingOption == 'Pipe to program'">{% trans "(Path to program)" %}</span></label> <label class="form-label">{% trans "Destination" %} <span ng-show="forwardingOption == 'Pipe to program'">{% trans "(Path to program)" %}</span></label>
<input placeholder="{% trans 'Enter destination email or program path' %}" type="text" class="form-control" ng-model="destinationEmail" required> <input placeholder="{% trans 'Enter destination email or program path' %}" type="text" class="form-control" ng-model="destinationEmail" required aria-label="{% trans 'Destination' %}">
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<div class="form-group mb-3"> <div class="form-group mb-3">
<label class="form-label">&nbsp;</label> <label class="form-label">&nbsp;</label>
<button type="button" ng-click="forwardEmail()" class="btn-primary" style="width: 100%;"> <button type="button" ng-click="forwardEmail()" class="btn-primary full-width-btn">
<i class="fas fa-plus-circle"></i> <i class="fas fa-plus-circle"></i>
{% trans "Add Forwarding" %} {% trans "Add Forwarding" %}
</button> </button>
@ -427,17 +461,17 @@
<table class="forwarding-table"> <table class="forwarding-table">
<thead> <thead>
<tr> <tr>
<th style="width: 10%;">{% trans "ID" %}</th> <th class="table-id">{% trans "ID" %}</th>
<th style="width: 35%;">{% trans "Source" %}</th> <th class="table-source">{% trans "Source" %}</th>
<th style="width: 40%;">{% trans "Destination" %}</th> <th class="table-destination">{% trans "Destination" %}</th>
<th style="width: 15%; text-align: center;">{% trans "Actions" %}</th> <th class="table-actions">{% trans "Actions" %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr ng-repeat="record in records track by $index"> <tr ng-repeat="record in records track by $index">
<td><strong ng-bind="record.id"></strong></td> <td><strong ng-bind="record.id"></strong></td>
<td> <td>
<i class="fas fa-envelope" style="color: var(--accent-color, #5b5fcf); margin-right: 0.5rem;"></i> <i class="fas fa-envelope email-icon"></i>
<span ng-bind="record.source"></span> <span ng-bind="record.source"></span>
</td> </td>
<td> <td>
@ -447,9 +481,9 @@
<span class="option-badge" ng-hide="record.destination.indexOf('@') > -1"> <span class="option-badge" ng-hide="record.destination.indexOf('@') > -1">
<i class="fas fa-terminal"></i> Program <i class="fas fa-terminal"></i> Program
</span> </span>
<span style="margin-left: 0.5rem;" ng-bind="record.destination"></span> <span class="destination-text" ng-bind="record.destination"></span>
</td> </td>
<td style="text-align: center;"> <td class="table-actions">
<button type="button" ng-click="deleteForwarding(record.source, record.destination)" class="btn-danger"> <button type="button" ng-click="deleteForwarding(record.source, record.destination)" class="btn-danger">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
{% trans "Delete" %} {% trans "Delete" %}

View File

@ -164,12 +164,10 @@
} }
/* Fix for Firefox on Windows */ /* Fix for Firefox on Windows */
@-moz-document url-prefix() {
select.form-control { select.form-control {
color: var(--text-dark, #2f3640) !important; color: var(--text-dark, #2f3640) !important;
text-shadow: none; text-shadow: none;
} }
}
/* Ensure selected option is always visible */ /* Ensure selected option is always visible */
select.form-control::-ms-value { select.form-control::-ms-value {
@ -397,6 +395,30 @@
flex-wrap: wrap; flex-wrap: wrap;
} }
.email-icon {
color: var(--accent-color, #5b5fcf);
margin-right: 0.5rem;
}
.email-address {
font-size: 0.9375rem;
color: var(--text-primary, #1e293b);
}
.disk-usage-badge {
padding: 0.375rem 0.875rem;
background: var(--bg-gradient, #f0f1ff);
color: var(--accent-color, #5b5fcf);
border-radius: 4px;
font-weight: 600;
font-size: 0.875rem;
}
.email-to-delete {
color: var(--danger-color, #ef4444);
font-weight: 600;
}
/* Modern Modal Styles */ /* Modern Modal Styles */
.modal-content { .modal-content {
border-radius: 12px; border-radius: 12px;
@ -593,7 +615,7 @@
<div class="col-md-6"> <div class="col-md-6">
<div class="form-group"> <div class="form-group">
<label class="form-label">{% trans "Select Domain" %}</label> <label class="form-label">{% trans "Select Domain" %}</label>
<select ng-change="populateCurrentRecords()" ng-model="selectedDomain" class="form-control"> <select ng-change="populateCurrentRecords()" ng-model="selectedDomain" class="form-control" aria-label="{% trans 'Select Domain' %}">
<option value="">{% trans "Choose a domain..." %}</option> <option value="">{% trans "Choose a domain..." %}</option>
{% for items in websiteList %} {% for items in websiteList %}
<option value="{{ items }}">{{ items }}</option> <option value="{{ items }}">{{ items }}</option>
@ -613,7 +635,7 @@
<a href="https://community.cyberpanel.net/t/6-self-signed-ssl-error-on-outlook-thunderbird/207" target="_blank" rel="noopener" style="color: var(--danger-text, #991b1b); text-decoration: underline;">{% trans "Learn more" %}</a> <a href="https://community.cyberpanel.net/t/6-self-signed-ssl-error-on-outlook-thunderbird/207" target="_blank" rel="noopener" style="color: var(--danger-text, #991b1b); text-decoration: underline;">{% trans "Learn more" %}</a>
</div> </div>
</div> </div>
<button ng-hide="mailConfigured==1" ng-click='fixMailSSL()' class="btn-primary mb-4"> <button type="button" ng-hide="mailConfigured==1" ng-click='fixMailSSL()' class="btn-primary mb-4">
<i class="fas fa-wrench"></i> <i class="fas fa-wrench"></i>
{% trans "Fix SSL Now" %} {% trans "Fix SSL Now" %}
</button> </button>
@ -731,11 +753,11 @@
<tbody> <tbody>
<tr ng-repeat="record in records track by $index"> <tr ng-repeat="record in records track by $index">
<td> <td>
<i class="fas fa-user-circle" style="color: var(--accent-color, #5b5fcf); margin-right: 0.5rem;"></i> <i class="fas fa-user-circle email-icon"></i>
<strong style="font-size: 0.9375rem; color: var(--text-primary, #1e293b);" ng-bind="record.email"></strong> <strong class="email-address" ng-bind="record.email"></strong>
</td> </td>
<td> <td>
<span style="padding: 0.375rem 0.875rem; background: var(--bg-gradient, #f0f1ff); color: var(--accent-color, #5b5fcf); border-radius: 4px; font-weight: 600; font-size: 0.875rem;"> <span class="disk-usage-badge">
{{ record.DiskUsage }} {{ record.DiskUsage }}
</span> </span>
</td> </td>
@ -767,7 +789,7 @@
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button> <button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title">Change Password <h4 class="modal-title">Change Password
<img ng-hide="cyberpanelLoading" src="{% static 'images/loading.gif' %}"> <img ng-hide="cyberpanelLoading" src="{% static 'images/loading.gif' %}" alt="Loading...">
</h4> </h4>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@ -775,7 +797,7 @@
<div ng-hide="installationDetailsForm" class="form-group"> <div ng-hide="installationDetailsForm" class="form-group">
<label class="col-sm-3 control-label">{% trans "Email" %}</label> <label class="col-sm-3 control-label">{% trans "Email" %}</label>
<div class="col-sm-6"> <div class="col-sm-6">
<input name="name" type="text" class="form-control" ng-model="email" readonly> <input name="name" type="text" class="form-control" ng-model="email" readonly aria-label="Email address">
</div> </div>
</div> </div>
@ -784,7 +806,7 @@
<div ng-hide="installationDetailsForm" class="form-group"> <div ng-hide="installationDetailsForm" class="form-group">
<label class="col-sm-3 control-label">{% trans "Password" %}</label> <label class="col-sm-3 control-label">{% trans "Password" %}</label>
<div class="col-sm-6"> <div class="col-sm-6">
<input type="password" class="form-control" ng-model="$parent.password"> <input type="password" class="form-control" ng-model="$parent.password" aria-label="Password">
</div> </div>
</div> </div>
</form> </form>
@ -837,7 +859,7 @@
<p style="margin: 0.5rem 0 0 0;">{% trans "This will permanently delete the email account and all associated data including emails, folders, and settings." %}</p> <p style="margin: 0.5rem 0 0 0;">{% trans "This will permanently delete the email account and all associated data including emails, folders, and settings." %}</p>
</div> </div>
</div> </div>
<p><strong>{% trans "Email to delete:" %}</strong> <span style="color: var(--danger-color, #ef4444); font-weight: 600;">{$ emailToDelete $}</span></p> <p><strong>{% trans "Email to delete:" %}</strong> <span class="email-to-delete">{$ emailToDelete $}</span></p>
<p>{% trans "Are you sure you want to continue?" %}</p> <p>{% trans "Are you sure you want to continue?" %}</p>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">

View File

@ -208,6 +208,7 @@
color: var(--text-primary, #2f3640); color: var(--text-primary, #2f3640);
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
-webkit-user-select: none;
user-select: none; user-select: none;
display: block; display: block;
margin-bottom: 4px; margin-bottom: 4px;

View File

@ -411,7 +411,7 @@
<div class="package-selector-card"> <div class="package-selector-card">
<div class="form-group"> <div class="form-group">
<label class="form-label">{% trans "Select Package to Delete" %}</label> <label class="form-label">{% trans "Select Package to Delete" %}</label>
<select ng-change="fetchPackageDetails()" ng-model="packageToBeDeleted" class="form-control"> <select ng-change="fetchPackageDetails()" ng-model="packageToBeDeleted" class="form-control" aria-label="{% trans 'Select Package to Delete' %}">
<option value="">-- {% trans "Select a package" %} --</option> <option value="">-- {% trans "Select a package" %} --</option>
{% for items in packageList %} {% for items in packageList %}
<option>{{ items }}</option> <option>{{ items }}</option>

View File

@ -139,6 +139,10 @@
color: var(--warning-dark); color: var(--warning-dark);
} }
.status-icon {
font-size: 8px;
}
/* Action buttons */ /* Action buttons */
.action-buttons { .action-buttons {
display: flex; display: flex;
@ -344,6 +348,7 @@
color: var(--text-primary); color: var(--text-primary);
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
-webkit-user-select: none;
user-select: none; user-select: none;
display: block; display: block;
margin-bottom: 4px; margin-bottom: 4px;
@ -483,7 +488,7 @@
<td>{$ record.ftpAccounts $}</td> <td>{$ record.ftpAccounts $}</td>
<td> <td>
<span class="status-badge" ng-class="{'enabled': record.allowFullDomain, 'disabled': !record.allowFullDomain}"> <span class="status-badge" ng-class="{'enabled': record.allowFullDomain, 'disabled': !record.allowFullDomain}">
<i class="fas fa-circle" style="font-size: 8px;"></i> <i class="fas fa-circle status-icon"></i>
<span ng-if="record.allowFullDomain">{% trans "Enabled" %}</span> <span ng-if="record.allowFullDomain">{% trans "Enabled" %}</span>
<span ng-if="!record.allowFullDomain">{% trans "Disabled" %}</span> <span ng-if="!record.allowFullDomain">{% trans "Disabled" %}</span>
</span> </span>
@ -526,7 +531,7 @@
<form name="editPackageForm"> <form name="editPackageForm">
<div class="form-group"> <div class="form-group">
<label class="form-label">{% trans "Package Name" %}</label> <label class="form-label">{% trans "Package Name" %}</label>
<input type="text" class="form-control" ng-model="name" readonly> <input type="text" class="form-control" ng-model="name" readonly aria-label="{% trans 'Package Name' %}">
</div> </div>
<div class="form-section"> <div class="form-section">
@ -537,14 +542,14 @@
<div class="form-group"> <div class="form-group">
<label class="form-label">{% trans "Domains" %}</label> <label class="form-label">{% trans "Domains" %}</label>
<input type="number" class="form-control" ng-model="allowedDomains" required> <input type="number" class="form-control" ng-model="allowedDomains" required aria-label="{% trans 'Number of domains allowed' %}">
<div class="help-text">{% trans "Number of domains allowed (0 = Unlimited)" %}</div> <div class="help-text">{% trans "Number of domains allowed (0 = Unlimited)" %}</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">{% trans "Disk Space" %}</label> <label class="form-label">{% trans "Disk Space" %}</label>
<div class="input-group"> <div class="input-group">
<input type="number" class="form-control" ng-model="diskSpace" required> <input type="number" class="form-control" ng-model="diskSpace" required aria-label="{% trans 'Disk space in MB' %}">
<span class="input-suffix">MB</span> <span class="input-suffix">MB</span>
</div> </div>
<div class="help-text">{% trans "Disk space in MB (0 = Unlimited)" %}</div> <div class="help-text">{% trans "Disk space in MB (0 = Unlimited)" %}</div>
@ -553,7 +558,7 @@
<div class="form-group"> <div class="form-group">
<label class="form-label">{% trans "Bandwidth" %}</label> <label class="form-label">{% trans "Bandwidth" %}</label>
<div class="input-group"> <div class="input-group">
<input type="number" class="form-control" ng-model="bandwidth" required> <input type="number" class="form-control" ng-model="bandwidth" required aria-label="{% trans 'Monthly bandwidth in MB' %}">
<span class="input-suffix">MB</span> <span class="input-suffix">MB</span>
</div> </div>
<div class="help-text">{% trans "Monthly bandwidth in MB (0 = Unlimited)" %}</div> <div class="help-text">{% trans "Monthly bandwidth in MB (0 = Unlimited)" %}</div>
@ -568,17 +573,17 @@
<div class="form-group"> <div class="form-group">
<label class="form-label">{% trans "Email Accounts" %}</label> <label class="form-label">{% trans "Email Accounts" %}</label>
<input type="number" class="form-control" ng-model="emails" required> <input type="number" class="form-control" ng-model="emails" required aria-label="{% trans 'Number of email accounts' %}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">{% trans "Databases" %}</label> <label class="form-label">{% trans "Databases" %}</label>
<input type="number" class="form-control" ng-model="dataBases" required> <input type="number" class="form-control" ng-model="dataBases" required aria-label="{% trans 'Number of databases' %}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">{% trans "FTP Accounts" %}</label> <label class="form-label">{% trans "FTP Accounts" %}</label>
<input type="number" class="form-control" ng-model="ftpAccounts" required> <input type="number" class="form-control" ng-model="ftpAccounts" required aria-label="{% trans 'Number of FTP accounts' %}">
</div> </div>
</div> </div>

View File

@ -243,6 +243,7 @@
color: var(--text-primary, #2f3640); color: var(--text-primary, #2f3640);
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
-webkit-user-select: none;
user-select: none; user-select: none;
display: block; display: block;
margin-bottom: 4px; margin-bottom: 4px;
@ -393,7 +394,7 @@
<div class="package-selector-card"> <div class="package-selector-card">
<div class="form-group"> <div class="form-group">
<label class="form-label">{% trans "Select Package to Modify" %}</label> <label class="form-label">{% trans "Select Package to Modify" %}</label>
<select ng-change="fetchDetails()" ng-model="packageToBeModified" class="form-control"> <select ng-change="fetchDetails()" ng-model="packageToBeModified" class="form-control" aria-label="{% trans 'Select Package to Modify' %}">
<option value="">-- {% trans "Select a package" %} --</option> <option value="">-- {% trans "Select a package" %} --</option>
{% for items in packList %} {% for items in packList %}
<option>{{ items }}</option> <option>{{ items }}</option>
@ -417,7 +418,7 @@
<div class="form-group"> <div class="form-group">
<label class="form-label">{% trans "Domains" %}</label> <label class="form-label">{% trans "Domains" %}</label>
<input type="number" class="form-control" ng-model="allowedDomains" required> <input type="number" class="form-control" ng-model="allowedDomains" required aria-label="{% trans 'Number of domains allowed' %}">
<div class="help-text">{% trans "Number of domains allowed (0 = Unlimited)" %}</div> <div class="help-text">{% trans "Number of domains allowed (0 = Unlimited)" %}</div>
</div> </div>
@ -425,7 +426,7 @@
<div class="form-group"> <div class="form-group">
<label class="form-label">{% trans "Disk Space" %}</label> <label class="form-label">{% trans "Disk Space" %}</label>
<div class="input-group"> <div class="input-group">
<input type="number" class="form-control" ng-model="diskSpace" required> <input type="number" class="form-control" ng-model="diskSpace" required aria-label="{% trans 'Disk space in MB' %}">
<span class="input-suffix">MB</span> <span class="input-suffix">MB</span>
</div> </div>
<div class="help-text">{% trans "Disk space in MB (0 = Unlimited)" %}</div> <div class="help-text">{% trans "Disk space in MB (0 = Unlimited)" %}</div>
@ -434,7 +435,7 @@
<div class="form-group"> <div class="form-group">
<label class="form-label">{% trans "Bandwidth" %}</label> <label class="form-label">{% trans "Bandwidth" %}</label>
<div class="input-group"> <div class="input-group">
<input type="number" class="form-control" ng-model="bandwidth" required> <input type="number" class="form-control" ng-model="bandwidth" required aria-label="{% trans 'Monthly bandwidth in MB' %}">
<span class="input-suffix">MB</span> <span class="input-suffix">MB</span>
</div> </div>
<div class="help-text">{% trans "Monthly bandwidth in MB (0 = Unlimited)" %}</div> <div class="help-text">{% trans "Monthly bandwidth in MB (0 = Unlimited)" %}</div>
@ -451,20 +452,20 @@
<div class="form-row"> <div class="form-row">
<div class="form-group"> <div class="form-group">
<label class="form-label">{% trans "FTP Accounts" %}</label> <label class="form-label">{% trans "FTP Accounts" %}</label>
<input type="number" class="form-control" ng-model="ftpAccounts" required> <input type="number" class="form-control" ng-model="ftpAccounts" required aria-label="{% trans 'Number of FTP accounts' %}">
<div class="help-text">{% trans "Number of FTP accounts" %}</div> <div class="help-text">{% trans "Number of FTP accounts" %}</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">{% trans "Databases" %}</label> <label class="form-label">{% trans "Databases" %}</label>
<input type="number" class="form-control" ng-model="dataBases" required> <input type="number" class="form-control" ng-model="dataBases" required aria-label="{% trans 'Number of MySQL databases' %}">
<div class="help-text">{% trans "Number of MySQL databases" %}</div> <div class="help-text">{% trans "Number of MySQL databases" %}</div>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">{% trans "Email Accounts" %}</label> <label class="form-label">{% trans "Email Accounts" %}</label>
<input type="number" class="form-control" ng-model="emails" required> <input type="number" class="form-control" ng-model="emails" required aria-label="{% trans 'Number of email accounts' %}">
<div class="help-text">{% trans "Number of email accounts" %}</div> <div class="help-text">{% trans "Number of email accounts" %}</div>
</div> </div>
</div> </div>

View File

@ -37,3 +37,4 @@ asyncssh==2.21.0
python-jose==3.4.0 python-jose==3.4.0
websockets==15.0.1 websockets==15.0.1
PyJWT PyJWT
python-dotenv==1.0.0

View File

@ -182,6 +182,7 @@
width: 60px; width: 60px;
height: 60px; height: 60px;
background: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.2);
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
border-radius: 12px; border-radius: 12px;
display: flex; display: flex;
@ -190,6 +191,14 @@
box-shadow: 0 4px 12px rgba(0,0,0,0.1); box-shadow: 0 4px 12px rgba(0,0,0,0.1);
} }
.package-icon i {
font-size: 1.75rem;
}
.loading-spinner-inline {
margin-left: 1rem;
}
.page-subtitle { .page-subtitle {
font-size: 1.125rem; font-size: 1.125rem;
color: var(--text-on-gradient); color: var(--text-on-gradient);
@ -207,6 +216,7 @@
.stat-badge { .stat-badge {
background: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.2);
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
padding: 0.5rem 1.5rem; padding: 0.5rem 1.5rem;
border-radius: 20px; border-radius: 20px;
@ -560,6 +570,7 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: 1000; z-index: 1000;
-webkit-backdrop-filter: blur(4px);
backdrop-filter: blur(4px); backdrop-filter: blur(4px);
} }
@ -760,7 +771,7 @@
<div class="header-content"> <div class="header-content">
<h1 class="page-title"> <h1 class="page-title">
<div class="package-icon"> <div class="package-icon">
<i class="fas fa-cube" style="font-size: 1.75rem;"></i> <i class="fas fa-cube"></i>
</div> </div>
{% trans "System Package Manager" %} {% trans "System Package Manager" %}
</h1> </h1>
@ -786,15 +797,15 @@
<div class="tabs-container"> <div class="tabs-container">
<!-- Tab Navigation --> <!-- Tab Navigation -->
<div class="modern-tabs"> <div class="modern-tabs">
<button class="tab-item active" ng-click="fetchPackages('upgrade')" data-tab="updates"> <button type="button" class="tab-item active" ng-click="fetchPackages('upgrade')" data-tab="updates" aria-label="{% trans 'View available updates' %}">
<i class="fas fa-sync-alt tab-icon"></i> <i class="fas fa-sync-alt tab-icon"></i>
{% trans "Available Updates" %} {% trans "Available Updates" %}
</button> </button>
<button class="tab-item" ng-click="fetchPackages()" data-tab="all"> <button type="button" class="tab-item" ng-click="fetchPackages()" data-tab="all" aria-label="{% trans 'View all packages' %}">
<i class="fas fa-list tab-icon"></i> <i class="fas fa-list tab-icon"></i>
{% trans "All Packages" %} {% trans "All Packages" %}
</button> </button>
<button class="tab-item" ng-click="fetchPackages('CyberPanel')" data-tab="cyberpanel"> <button type="button" class="tab-item" ng-click="fetchPackages('CyberPanel')" data-tab="cyberpanel" aria-label="{% trans 'View CyberPanel packages' %}">
<i class="fas fa-shield-alt tab-icon"></i> <i class="fas fa-shield-alt tab-icon"></i>
{% trans "CyberPanel Packages" %} {% trans "CyberPanel Packages" %}
</button> </button>
@ -807,20 +818,20 @@
<div class="search-box"> <div class="search-box">
<i class="fas fa-search search-icon"></i> <i class="fas fa-search search-icon"></i>
<input type="text" class="search-input" placeholder="{% trans 'Search packages...' %}" <input type="text" class="search-input" placeholder="{% trans 'Search packages...' %}"
ng-model="packSearch"> ng-model="packSearch" aria-label="{% trans 'Search packages' %}">
</div> </div>
<div class="control-group"> <div class="control-group">
<select ng-model="recordsToShow" class="select-control" <select ng-model="recordsToShow" class="select-control"
ng-change="fetchPackages(currentTab)"> ng-change="fetchPackages(currentTab)" aria-label="{% trans 'Records per page' %}">
<option value="10">10 {% trans "per page" %}</option> <option value="10">10 {% trans "per page" %}</option>
<option value="50">50 {% trans "per page" %}</option> <option value="50">50 {% trans "per page" %}</option>
<option value="100">100 {% trans "per page" %}</option> <option value="100">100 {% trans "per page" %}</option>
<option value="500">500 {% trans "per page" %}</option> <option value="500">500 {% trans "per page" %}</option>
</select> </select>
<button class="btn btn-success" ng-click="updatePackage('all')" <button type="button" class="btn btn-success" ng-click="updatePackage('all')"
ng-show="currentTab === 'upgrade'"> ng-show="currentTab === 'upgrade'" aria-label="{% trans 'Update all packages' %}">
<i class="fas fa-download"></i> <i class="fas fa-download"></i>
{% trans "Update All" %} {% trans "Update All" %}
</button> </button>
@ -865,24 +876,24 @@
</span> </span>
</td> </td>
<td> <td>
<a class="lock-toggle" <button type="button" class="lock-toggle"
ng-class="{'locked': package.lock == 1, 'unlocked': package.lock == 0}" ng-class="{'locked': package.lock == 1, 'unlocked': package.lock == 0}"
ng-click="lockStatus(package.package, package.lock == 1 ? 1 : 0)" ng-click="lockStatus(package.package, package.lock == 1 ? 1 : 0)"
title="{$ package.lock == 1 ? 'Package is locked' : 'Package is unlocked' $}"> aria-label="{$ package.lock == 1 ? 'Unlock package' : 'Lock package' $}">
<i class="fas" <i class="fas"
ng-class="{'fa-lock': package.lock == 1, 'fa-lock-open': package.lock == 0}"></i> ng-class="{'fa-lock': package.lock == 1, 'fa-lock-open': package.lock == 0}"></i>
</a> </button>
</td> </td>
<td> <td>
<div class="action-buttons"> <div class="action-buttons">
<button class="btn btn-info btn-sm" <button type="button" class="btn btn-info btn-sm"
ng-click="showPackageDetails(package.package)"> ng-click="showPackageDetails(package.package)" aria-label="{% trans 'View package details' %}">
<i class="fas fa-info-circle"></i> <i class="fas fa-info-circle"></i>
{% trans "Details" %} {% trans "Details" %}
</button> </button>
<button class="btn btn-primary btn-sm" <button type="button" class="btn btn-primary btn-sm"
ng-click="showUpdateModal(package.package)" ng-click="showUpdateModal(package.package)"
ng-show="currentTab === 'upgrade' || currentTab === 'cyberpanel'"> ng-show="currentTab === 'upgrade' || currentTab === 'cyberpanel'" aria-label="{% trans 'Update package' %}">
<i class="fas fa-download"></i> <i class="fas fa-download"></i>
{% trans "Update" %} {% trans "Update" %}
</button> </button>
@ -912,7 +923,7 @@
<div class="pagination-controls"> <div class="pagination-controls">
<span>{% trans "Page:" %}</span> <span>{% trans "Page:" %}</span>
<select ng-model="currentPage" class="page-select" <select ng-model="currentPage" class="page-select"
ng-change="fetchPackages(currentTab)"> ng-change="fetchPackages(currentTab)" aria-label="{% trans 'Select page' %}">
<option ng-repeat="page in pagination" value="{$ $index + 1 $}"> <option ng-repeat="page in pagination" value="{$ $index + 1 $}">
{$ $index + 1 $} {$ $index + 1 $}
</option> </option>
@ -931,7 +942,7 @@
<i class="fas fa-info-circle"></i> <i class="fas fa-info-circle"></i>
{% trans "Package Details:" %} {$ selectedPackage $} {% trans "Package Details:" %} {$ selectedPackage $}
</h3> </h3>
<button class="modal-close" ng-click="closeDetails()"> <button type="button" class="modal-close" ng-click="closeDetails()" aria-label="{% trans 'Close details modal' %}">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
</button> </button>
</div> </div>
@ -948,9 +959,9 @@
<h3 class="modal-title"> <h3 class="modal-title">
<i class="fas fa-download"></i> <i class="fas fa-download"></i>
{% trans "Updating Package:" %} {$ updatingPackage $} {% trans "Updating Package:" %} {$ updatingPackage $}
<span ng-hide="cyberpanelLoading" class="loading-spinner" style="margin-left: 1rem;"></span> <span ng-hide="cyberpanelLoading" class="loading-spinner loading-spinner-inline"></span>
</h3> </h3>
<button class="modal-close" ng-click="closeUpdate()" ng-disabled="!updateComplete"> <button type="button" class="modal-close" ng-click="closeUpdate()" ng-disabled="!updateComplete" aria-label="{% trans 'Close update modal' %}">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
</button> </button>
</div> </div>

View File

@ -1729,6 +1729,8 @@ app.controller('scheduleBackup', function ($scope, $http, $window) {
$scope.allSites = response.data.allSites; $scope.allSites = response.data.allSites;
$scope.lastRun = response.data.lastRun; $scope.lastRun = response.data.lastRun;
$scope.currentStatus = response.data.currentStatus; $scope.currentStatus = response.data.currentStatus;
$scope.backupFrequency = response.data.currently;
$scope.backupRetention = response.data.retention;
} else { } else {
new PNotify({ new PNotify({

View File

@ -0,0 +1,192 @@
# CyberPanel Secure Installation Guide
## Overview
This document describes the secure installation process for CyberPanel that eliminates hardcoded passwords and implements environment-based configuration.
## Security Improvements
### ✅ **Fixed Security Vulnerabilities**
1. **Hardcoded Database Passwords** - Now generated securely during installation
2. **Hardcoded Django Secret Key** - Now generated using cryptographically secure random generation
3. **Environment Variables** - All sensitive configuration moved to `.env` file
4. **File Permissions** - `.env` file set to 600 (owner read/write only)
### 🔐 **Security Features**
- **Cryptographically Secure Passwords**: Uses Python's `secrets` module for password generation
- **Environment-based Configuration**: Sensitive data stored in `.env` file, not in code
- **Secure File Permissions**: Environment files protected with 600 permissions
- **Credential Backup**: Automatic backup of credentials for recovery
- **Fallback Security**: Maintains backward compatibility with fallback method
## Installation Process
### 1. **Automatic Secure Installation**
The installation script now automatically:
1. Generates secure random passwords for:
- MySQL root user
- CyberPanel database user
- Django secret key
2. Creates `.env` file with secure configuration:
```bash
# Generated during installation
SECRET_KEY=your_64_character_secure_key
DB_PASSWORD=your_24_character_secure_password
ROOT_DB_PASSWORD=your_24_character_secure_password
```
3. Creates `.env.backup` file for credential recovery
4. Sets secure file permissions (600) on all environment files
### 2. **Manual Installation** (if needed)
If you need to manually generate environment configuration:
```bash
cd /usr/local/CyberCP
python install/env_generator.py /usr/local/CyberCP
```
## File Structure
```
/usr/local/CyberCP/
├── .env # Main environment configuration (600 permissions)
├── .env.backup # Credential backup (600 permissions)
├── .env.template # Template for manual configuration
├── .gitignore # Prevents .env files from being committed
└── CyberCP/
└── settings.py # Updated to use environment variables
```
## Security Best Practices
### ✅ **Do's**
- Keep `.env` and `.env.backup` files secure
- Record credentials from `.env.backup` and delete the file after installation
- Use strong, unique passwords for production deployments
- Regularly rotate database passwords
- Monitor access to environment files
### ❌ **Don'ts**
- Never commit `.env` files to version control
- Don't share `.env` files via insecure channels
- Don't use default passwords in production
- Don't leave `.env.backup` files on the system after recording credentials
## Recovery
### **Lost Credentials**
If you lose your database credentials:
1. Check if `.env.backup` file exists:
```bash
sudo cat /usr/local/CyberCP/.env.backup
```
2. If backup doesn't exist, you'll need to reset MySQL passwords using MySQL recovery procedures
### **Regenerate Environment**
To regenerate environment configuration:
```bash
cd /usr/local/CyberCP
sudo python install/env_generator.py /usr/local/CyberCP
```
## Configuration Options
### **Environment Variables**
| Variable | Description | Default |
|----------|-------------|---------|
| `SECRET_KEY` | Django secret key | Generated (64 chars) |
| `DB_PASSWORD` | CyberPanel DB password | Generated (24 chars) |
| `ROOT_DB_PASSWORD` | MySQL root password | Generated (24 chars) |
| `DEBUG` | Debug mode | False |
| `ALLOWED_HOSTS` | Allowed hosts | localhost,127.0.0.1,hostname |
### **Custom Configuration**
To use custom passwords during installation:
```bash
python install/env_generator.py /usr/local/CyberCP "your_root_password" "your_db_password"
```
## Troubleshooting
### **Installation Fails**
If the new secure installation fails:
1. Check installation logs for error messages
2. The system will automatically fallback to the original installation method
3. Verify Python dependencies are installed:
```bash
pip install python-dotenv
```
### **Environment Loading Issues**
If Django can't load environment variables:
1. Ensure `.env` file exists and has correct permissions:
```bash
ls -la /usr/local/CyberCP/.env
# Should show: -rw------- 1 root root
```
2. Install python-dotenv if missing:
```bash
pip install python-dotenv
```
## Migration from Old Installation
### **Existing Installations**
For existing CyberPanel installations with hardcoded passwords:
1. **Backup current configuration**:
```bash
cp /usr/local/CyberCP/CyberCP/settings.py /usr/local/CyberCP/CyberCP/settings.py.backup
```
2. **Generate new environment configuration**:
```bash
cd /usr/local/CyberCP
python install/env_generator.py /usr/local/CyberCP
```
3. **Update settings.py** (already done in new installations):
- The settings.py file now supports environment variables
- It will fallback to hardcoded values if .env is not available
4. **Test the configuration**:
```bash
cd /usr/local/CyberCP
python manage.py check
```
## Support
For issues with the secure installation:
1. Check the installation logs
2. Verify file permissions
3. Ensure all dependencies are installed
4. Review the fallback installation method if needed
---
**Security Notice**: This installation method significantly improves security by eliminating hardcoded credentials. Always ensure proper file permissions and secure handling of environment files.