added UI php version for enrollment
This commit is contained in:
parent
4ce79cdf33
commit
5e7f322877
|
|
@ -26,6 +26,7 @@ NC='\033[0m' # No Color
|
||||||
# Default API endpoint (can be overridden)
|
# Default API endpoint (can be overridden)
|
||||||
DEFAULT_API_ENDPOINT="https://backend.zitinexus.com"
|
DEFAULT_API_ENDPOINT="https://backend.zitinexus.com"
|
||||||
|
|
||||||
|
|
||||||
# Initialize variables to prevent unbound variable errors
|
# Initialize variables to prevent unbound variable errors
|
||||||
CALLBACK_URL=""
|
CALLBACK_URL=""
|
||||||
JWT=""
|
JWT=""
|
||||||
|
|
@ -37,7 +38,9 @@ CONTROLLER_ENDPOINT=""
|
||||||
ROLE_ATTRIBUTES=""
|
ROLE_ATTRIBUTES=""
|
||||||
HASH_KEY=""
|
HASH_KEY=""
|
||||||
API_ENDPOINT=""
|
API_ENDPOINT=""
|
||||||
|
# Example modification (Automated installation , uncomment below)
|
||||||
|
#API_ENDPOINT="https://backend.zitinexus.com"
|
||||||
|
#HASH_KEY="your-hash-key-here"
|
||||||
# Logging function
|
# Logging function
|
||||||
log() {
|
log() {
|
||||||
local level=$1
|
local level=$1
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,178 @@
|
||||||
|
# ZitiNexus Router Enrollment UI - Installation Guide
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Ubuntu 22.04 or 24.04 LTS
|
||||||
|
- Root/sudo access
|
||||||
|
- Internet connectivity
|
||||||
|
|
||||||
|
### Automated Installation
|
||||||
|
|
||||||
|
1. **Download and extract the UI files to your server**
|
||||||
|
|
||||||
|
2. **Run the installation script:**
|
||||||
|
```bash
|
||||||
|
cd UI
|
||||||
|
sudo chmod +x install.sh
|
||||||
|
sudo ./install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Follow the prompts to select your web server (Apache or Nginx)**
|
||||||
|
|
||||||
|
4. **Access the interface:**
|
||||||
|
- URL: `http://ziti-enrollment.local`
|
||||||
|
- Username: `admin`
|
||||||
|
- Password: `admin123`
|
||||||
|
|
||||||
|
### Manual Installation
|
||||||
|
|
||||||
|
If you prefer manual installation, follow the detailed steps in [README.md](README.md).
|
||||||
|
|
||||||
|
## Post-Installation Steps
|
||||||
|
|
||||||
|
### 1. Change Default Password (IMPORTANT)
|
||||||
|
|
||||||
|
Edit `/var/www/ziti-enrollment/includes/config.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Change this line:
|
||||||
|
define('ADMIN_PASSWORD_HASH', password_hash('your-new-secure-password', PASSWORD_DEFAULT));
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configure for Production
|
||||||
|
|
||||||
|
#### Enable HTTPS
|
||||||
|
```bash
|
||||||
|
# Install SSL certificate (example with Let's Encrypt)
|
||||||
|
sudo apt install certbot python3-certbot-apache # or python3-certbot-nginx
|
||||||
|
sudo certbot --apache -d your-domain.com # or --nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Secure File Permissions
|
||||||
|
```bash
|
||||||
|
sudo chmod 600 /var/www/ziti-enrollment/includes/config.php
|
||||||
|
sudo chown root:www-data /var/www/ziti-enrollment/includes/config.php
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Configure Firewall
|
||||||
|
```bash
|
||||||
|
sudo ufw allow 80/tcp
|
||||||
|
sudo ufw allow 443/tcp
|
||||||
|
sudo ufw enable
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Test the Installation
|
||||||
|
|
||||||
|
1. **Access the web interface**
|
||||||
|
2. **Login with your credentials**
|
||||||
|
3. **Check system status on the dashboard**
|
||||||
|
4. **Test enrollment with a valid hash key**
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
#### 1. Permission Denied Errors
|
||||||
|
```bash
|
||||||
|
# Fix ownership
|
||||||
|
sudo chown -R www-data:www-data /var/www/ziti-enrollment
|
||||||
|
|
||||||
|
# Fix permissions
|
||||||
|
sudo chmod -R 755 /var/www/ziti-enrollment
|
||||||
|
sudo chmod -R 777 /var/www/ziti-enrollment/logs /var/www/ziti-enrollment/temp
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. PHP Functions Disabled
|
||||||
|
```bash
|
||||||
|
# Check disabled functions
|
||||||
|
php -r "echo ini_get('disable_functions');"
|
||||||
|
|
||||||
|
# Edit PHP configuration
|
||||||
|
sudo nano /etc/php/8.1/apache2/php.ini # or /etc/php/8.1/fpm/php.ini
|
||||||
|
|
||||||
|
# Remove exec, shell_exec, proc_open from disable_functions line
|
||||||
|
# Restart web server
|
||||||
|
sudo systemctl restart apache2 # or nginx and php8.1-fpm
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Sudo Access Issues
|
||||||
|
```bash
|
||||||
|
# Test sudo access
|
||||||
|
sudo -u www-data sudo -l
|
||||||
|
|
||||||
|
# If issues, recreate sudoers file
|
||||||
|
sudo tee /etc/sudoers.d/ziti-enrollment << 'EOF'
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/apt-get, /usr/bin/systemctl, /usr/bin/mkdir, /usr/bin/chmod, /usr/bin/chown, /usr/bin/curl, /usr/bin/gpg, /usr/bin/ziti, /usr/bin/which, /usr/bin/hostname, /usr/bin/uname, /usr/bin/lsb_release
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Validate
|
||||||
|
sudo visudo -c
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Web Server Not Starting
|
||||||
|
```bash
|
||||||
|
# Check status
|
||||||
|
sudo systemctl status apache2 # or nginx
|
||||||
|
|
||||||
|
# Check logs
|
||||||
|
sudo journalctl -u apache2 -f # or nginx
|
||||||
|
|
||||||
|
# Check configuration
|
||||||
|
sudo apache2ctl configtest # or nginx -t
|
||||||
|
```
|
||||||
|
|
||||||
|
### Log Files
|
||||||
|
|
||||||
|
- **UI Logs**: `/var/www/ziti-enrollment/logs/ui-enrollment.log`
|
||||||
|
- **System Logs**: `/var/log/ziti-router-enrollment.log`
|
||||||
|
- **Web Server Logs**:
|
||||||
|
- Apache: `/var/log/apache2/ziti-enrollment_error.log`
|
||||||
|
- Nginx: `/var/log/nginx/error.log`
|
||||||
|
- **PHP Logs**: `/var/log/php_errors.log`
|
||||||
|
|
||||||
|
## Security Checklist
|
||||||
|
|
||||||
|
- [ ] Changed default password
|
||||||
|
- [ ] Configured HTTPS
|
||||||
|
- [ ] Set proper file permissions
|
||||||
|
- [ ] Configured firewall
|
||||||
|
- [ ] Restricted network access (if needed)
|
||||||
|
- [ ] Regular security updates scheduled
|
||||||
|
- [ ] Log monitoring configured
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For technical support:
|
||||||
|
1. Check the logs for error messages
|
||||||
|
2. Verify system requirements are met
|
||||||
|
3. Test individual components (web server, PHP, sudo access)
|
||||||
|
4. Review the troubleshooting section
|
||||||
|
5. Consult the main [README.md](README.md) for detailed information
|
||||||
|
|
||||||
|
## Uninstallation
|
||||||
|
|
||||||
|
To remove the UI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Stop and disable web server
|
||||||
|
sudo systemctl stop apache2 # or nginx php8.1-fpm
|
||||||
|
sudo systemctl disable apache2 # or nginx php8.1-fpm
|
||||||
|
|
||||||
|
# Remove files
|
||||||
|
sudo rm -rf /var/www/ziti-enrollment
|
||||||
|
sudo rm -f /etc/apache2/sites-available/ziti-enrollment.conf # or /etc/nginx/sites-available/ziti-enrollment
|
||||||
|
sudo rm -f /etc/apache2/sites-enabled/ziti-enrollment.conf # or /etc/nginx/sites-enabled/ziti-enrollment
|
||||||
|
sudo rm -f /etc/sudoers.d/ziti-enrollment
|
||||||
|
|
||||||
|
# Remove from hosts file
|
||||||
|
sudo sed -i '/ziti-enrollment.local/d' /etc/hosts
|
||||||
|
|
||||||
|
# Optionally remove packages
|
||||||
|
sudo apt remove apache2 php8.1 libapache2-mod-php8.1 # or nginx php8.1-fpm
|
||||||
|
sudo apt autoremove
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Note**: This UI complements the original bash script and provides the same functionality through a modern web interface. Both tools can coexist on the same system.
|
||||||
|
|
@ -0,0 +1,371 @@
|
||||||
|
# ZitiNexus Router Enrollment UI
|
||||||
|
|
||||||
|
A modern PHP-based web interface for enrolling OpenZiti routers using the ZitiNexus Portal. This UI provides a user-friendly alternative to the command-line enrollment script.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Modern Web Interface**: Clean, responsive design with real-time progress tracking
|
||||||
|
- **Authentication System**: Secure login with session management
|
||||||
|
- **System Status Monitoring**: Real-time display of Ziti CLI, service, and configuration status
|
||||||
|
- **Interactive Enrollment**: Step-by-step enrollment process with live progress updates
|
||||||
|
- **Input Validation**: Client-side and server-side validation for hash keys and API endpoints
|
||||||
|
- **Comprehensive Logging**: Detailed logs with different severity levels
|
||||||
|
- **Mobile Responsive**: Works on desktop, tablet, and mobile devices
|
||||||
|
|
||||||
|
## System Requirements
|
||||||
|
|
||||||
|
- **Operating System**: Ubuntu 22.04 or 24.04 LTS
|
||||||
|
- **Web Server**: Apache 2.4+ or Nginx 1.18+
|
||||||
|
- **PHP**: 8.0 or higher with extensions:
|
||||||
|
- curl
|
||||||
|
- json
|
||||||
|
- posix
|
||||||
|
- proc_open/exec functions enabled
|
||||||
|
- **Root Access**: Required for system operations (router installation, service management)
|
||||||
|
- **Internet Connectivity**: For downloading packages and API communication
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### 1. Install Web Server and PHP
|
||||||
|
|
||||||
|
#### For Apache:
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install apache2 php8.1 php8.1-curl php8.1-json libapache2-mod-php8.1
|
||||||
|
sudo systemctl enable apache2
|
||||||
|
sudo systemctl start apache2
|
||||||
|
```
|
||||||
|
|
||||||
|
#### For Nginx:
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install nginx php8.1-fpm php8.1-curl php8.1-json
|
||||||
|
sudo systemctl enable nginx php8.1-fpm
|
||||||
|
sudo systemctl start nginx php8.1-fpm
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Deploy the UI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create web directory
|
||||||
|
sudo mkdir -p /var/www/ziti-enrollment
|
||||||
|
|
||||||
|
# Copy UI files
|
||||||
|
sudo cp -r UI/* /var/www/ziti-enrollment/
|
||||||
|
|
||||||
|
# Set proper permissions
|
||||||
|
sudo chown -R www-data:www-data /var/www/ziti-enrollment
|
||||||
|
sudo chmod -R 755 /var/www/ziti-enrollment
|
||||||
|
sudo chmod -R 777 /var/www/ziti-enrollment/logs
|
||||||
|
sudo chmod -R 777 /var/www/ziti-enrollment/temp
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Configure Web Server
|
||||||
|
|
||||||
|
#### Apache Virtual Host:
|
||||||
|
```bash
|
||||||
|
sudo tee /etc/apache2/sites-available/ziti-enrollment.conf << 'EOF'
|
||||||
|
<VirtualHost *:80>
|
||||||
|
ServerName ziti-enrollment.local
|
||||||
|
DocumentRoot /var/www/ziti-enrollment/public
|
||||||
|
|
||||||
|
<Directory /var/www/ziti-enrollment/public>
|
||||||
|
AllowOverride All
|
||||||
|
Require all granted
|
||||||
|
DirectoryIndex index.php
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
ErrorLog ${APACHE_LOG_DIR}/ziti-enrollment_error.log
|
||||||
|
CustomLog ${APACHE_LOG_DIR}/ziti-enrollment_access.log combined
|
||||||
|
</VirtualHost>
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Enable site and rewrite module
|
||||||
|
sudo a2ensite ziti-enrollment.conf
|
||||||
|
sudo a2enmod rewrite
|
||||||
|
sudo systemctl reload apache2
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Nginx Configuration:
|
||||||
|
```bash
|
||||||
|
sudo tee /etc/nginx/sites-available/ziti-enrollment << 'EOF'
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name ziti-enrollment.local;
|
||||||
|
root /var/www/ziti-enrollment/public;
|
||||||
|
index index.php;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ \.php$ {
|
||||||
|
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
|
||||||
|
fastcgi_index index.php;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
|
||||||
|
include fastcgi_params;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ /\. {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Enable site
|
||||||
|
sudo ln -s /etc/nginx/sites-available/ziti-enrollment /etc/nginx/sites-enabled/
|
||||||
|
sudo systemctl reload nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Configure PHP for System Commands
|
||||||
|
|
||||||
|
Edit PHP configuration to allow system command execution:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Find PHP configuration file
|
||||||
|
php --ini
|
||||||
|
|
||||||
|
# Edit php.ini (example path)
|
||||||
|
sudo nano /etc/php/8.1/apache2/php.ini
|
||||||
|
|
||||||
|
# Ensure these functions are NOT in disable_functions:
|
||||||
|
# exec, shell_exec, system, proc_open, proc_close, proc_get_status
|
||||||
|
|
||||||
|
# Restart web server
|
||||||
|
sudo systemctl restart apache2 # or nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Set Up Sudo Access for Web Server
|
||||||
|
|
||||||
|
The web server needs to run system commands as root. Create a sudoers file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo tee /etc/sudoers.d/ziti-enrollment << 'EOF'
|
||||||
|
# Allow www-data to run system commands for Ziti enrollment
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/apt-get
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/systemctl
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/mkdir
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/chmod
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/chown
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/curl
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/gpg
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/ziti
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/which
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/hostname
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/uname
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/lsb_release
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Validate sudoers file
|
||||||
|
sudo visudo -c
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Update Hosts File (Optional)
|
||||||
|
|
||||||
|
For local testing, add the domain to your hosts file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "127.0.0.1 ziti-enrollment.local" | sudo tee -a /etc/hosts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Default Credentials
|
||||||
|
|
||||||
|
- **Username**: `admin`
|
||||||
|
- **Password**: `admin123`
|
||||||
|
|
||||||
|
**⚠️ Important**: Change the default password in production by modifying `UI/includes/config.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
define('ADMIN_PASSWORD_HASH', password_hash('your-new-password', PASSWORD_DEFAULT));
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Endpoint
|
||||||
|
|
||||||
|
The default API endpoint is set to `https://backend.zitinexus.com`. You can modify this in `UI/includes/config.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
define('DEFAULT_API_ENDPOINT', 'https://your-api-endpoint.com');
|
||||||
|
```
|
||||||
|
|
||||||
|
### File Paths
|
||||||
|
|
||||||
|
All file paths match the original bash script:
|
||||||
|
- Configuration: `/etc/zitirouter/`
|
||||||
|
- Certificates: `/etc/zitirouter/certs/`
|
||||||
|
- Logs: `/var/log/ziti-router-enrollment.log`
|
||||||
|
- Service: `/etc/systemd/system/ziti-router.service`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. **Access the Interface**
|
||||||
|
- Open your web browser
|
||||||
|
- Navigate to `http://ziti-enrollment.local` (or your configured domain)
|
||||||
|
- Login with the default credentials
|
||||||
|
|
||||||
|
2. **Check System Status**
|
||||||
|
- The dashboard shows current system status
|
||||||
|
- Ziti CLI installation status
|
||||||
|
- Router service status
|
||||||
|
- Configuration status
|
||||||
|
- System hostname
|
||||||
|
|
||||||
|
3. **Enroll a Router**
|
||||||
|
- Obtain a hash key from ZitiNexus Portal
|
||||||
|
- Enter the API endpoint (pre-filled with default)
|
||||||
|
- Paste the 32-character hash key
|
||||||
|
- Click "Start Enrollment"
|
||||||
|
- Monitor progress and logs in real-time
|
||||||
|
|
||||||
|
4. **Monitor Progress**
|
||||||
|
- Progress bar shows overall completion
|
||||||
|
- Step indicators show current phase
|
||||||
|
- Live log output shows detailed progress
|
||||||
|
- System status updates automatically after enrollment
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### Production Deployment
|
||||||
|
|
||||||
|
1. **Change Default Password**
|
||||||
|
```php
|
||||||
|
define('ADMIN_PASSWORD_HASH', password_hash('strong-password-here', PASSWORD_DEFAULT));
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Use HTTPS**
|
||||||
|
- Configure SSL/TLS certificates
|
||||||
|
- Redirect HTTP to HTTPS
|
||||||
|
- Update virtual host configuration
|
||||||
|
|
||||||
|
3. **Restrict Access**
|
||||||
|
- Use firewall rules to limit access
|
||||||
|
- Consider VPN or IP whitelisting
|
||||||
|
- Implement additional authentication if needed
|
||||||
|
|
||||||
|
4. **File Permissions**
|
||||||
|
```bash
|
||||||
|
sudo chmod 600 /var/www/ziti-enrollment/includes/config.php
|
||||||
|
sudo chown root:www-data /var/www/ziti-enrollment/includes/config.php
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Regular Updates**
|
||||||
|
- Keep PHP and web server updated
|
||||||
|
- Monitor security advisories
|
||||||
|
- Review logs regularly
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Permission Denied Errors**
|
||||||
|
```bash
|
||||||
|
# Check web server user
|
||||||
|
ps aux | grep apache2 # or nginx
|
||||||
|
|
||||||
|
# Ensure proper ownership
|
||||||
|
sudo chown -R www-data:www-data /var/www/ziti-enrollment
|
||||||
|
|
||||||
|
# Check sudoers configuration
|
||||||
|
sudo -u www-data sudo -l
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **PHP Function Disabled**
|
||||||
|
```bash
|
||||||
|
# Check disabled functions
|
||||||
|
php -r "echo ini_get('disable_functions');"
|
||||||
|
|
||||||
|
# Edit php.ini to remove exec, shell_exec, proc_open from disable_functions
|
||||||
|
sudo nano /etc/php/8.1/apache2/php.ini
|
||||||
|
sudo systemctl restart apache2
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **System Command Failures**
|
||||||
|
```bash
|
||||||
|
# Test sudo access
|
||||||
|
sudo -u www-data sudo systemctl status ziti-router
|
||||||
|
|
||||||
|
# Check system logs
|
||||||
|
sudo tail -f /var/log/syslog
|
||||||
|
sudo tail -f /var/log/ziti-router-enrollment.log
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Web Server Issues**
|
||||||
|
```bash
|
||||||
|
# Check web server status
|
||||||
|
sudo systemctl status apache2 # or nginx
|
||||||
|
|
||||||
|
# Check error logs
|
||||||
|
sudo tail -f /var/log/apache2/error.log
|
||||||
|
sudo tail -f /var/log/nginx/error.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### Debug Mode
|
||||||
|
|
||||||
|
Enable debug logging by modifying `UI/includes/config.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Add at the top of config.php
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
ini_set('log_errors', 1);
|
||||||
|
ini_set('error_log', '/var/log/php_errors.log');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Log Files
|
||||||
|
|
||||||
|
- **UI Logs**: `/var/www/ziti-enrollment/logs/ui-enrollment.log`
|
||||||
|
- **System Logs**: `/var/log/ziti-router-enrollment.log`
|
||||||
|
- **Web Server Logs**: `/var/log/apache2/` or `/var/log/nginx/`
|
||||||
|
- **PHP Logs**: `/var/log/php_errors.log`
|
||||||
|
|
||||||
|
## API Compatibility
|
||||||
|
|
||||||
|
This UI is fully compatible with the ZitiNexus Portal API and replicates all functionality of the original bash script:
|
||||||
|
|
||||||
|
- Router registration with hash key validation
|
||||||
|
- JWT token and configuration download
|
||||||
|
- OpenZiti CLI installation and setup
|
||||||
|
- Router enrollment and certificate generation
|
||||||
|
- Systemd service creation and management
|
||||||
|
- Status reporting back to portal
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### File Structure
|
||||||
|
```
|
||||||
|
UI/
|
||||||
|
├── public/
|
||||||
|
│ ├── index.php # Login page
|
||||||
|
│ └── dashboard.php # Main dashboard
|
||||||
|
├── includes/
|
||||||
|
│ ├── config.php # Configuration and utilities
|
||||||
|
│ ├── auth.php # Authentication handler
|
||||||
|
│ ├── api_client.php # API communication
|
||||||
|
│ └── enrollment.php # Core enrollment logic
|
||||||
|
├── assets/
|
||||||
|
│ ├── css/style.css # Styling
|
||||||
|
│ └── js/app.js # Frontend JavaScript
|
||||||
|
├── logs/ # UI-specific logs
|
||||||
|
├── temp/ # Temporary files
|
||||||
|
└── README.md # This file
|
||||||
|
```
|
||||||
|
|
||||||
|
### Contributing
|
||||||
|
|
||||||
|
1. Follow PSR-12 coding standards for PHP
|
||||||
|
2. Use modern JavaScript (ES6+)
|
||||||
|
3. Maintain responsive design principles
|
||||||
|
4. Add comprehensive error handling
|
||||||
|
5. Update documentation for any changes
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues related to:
|
||||||
|
- **UI functionality**: Check logs and configuration
|
||||||
|
- **ZitiNexus Portal**: Contact your portal administrator
|
||||||
|
- **OpenZiti**: Visit [OpenZiti Documentation](https://docs.openziti.io/)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project follows the same license as the original ZitiNexus Router Script.
|
||||||
|
|
@ -0,0 +1,490 @@
|
||||||
|
/* ZitiNexus Router Enrollment UI Styles */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--primary-color: #2563eb;
|
||||||
|
--primary-dark: #1d4ed8;
|
||||||
|
--secondary-color: #64748b;
|
||||||
|
--success-color: #10b981;
|
||||||
|
--warning-color: #f59e0b;
|
||||||
|
--error-color: #ef4444;
|
||||||
|
--background-color: #f8fafc;
|
||||||
|
--card-background: #ffffff;
|
||||||
|
--text-primary: #1e293b;
|
||||||
|
--text-secondary: #64748b;
|
||||||
|
--border-color: #e2e8f0;
|
||||||
|
--shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||||
|
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Login Page Styles */
|
||||||
|
.login-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
background: var(--card-background);
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header h1 {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 1.875rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header p {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: var(--primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background-color: var(--success-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning {
|
||||||
|
background-color: var(--warning-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background-color: var(--error-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alert Styles */
|
||||||
|
.alert {
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background-color: #dcfce7;
|
||||||
|
color: #166534;
|
||||||
|
border: 1px solid #bbf7d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-error {
|
||||||
|
background-color: #fef2f2;
|
||||||
|
color: #991b1b;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-warning {
|
||||||
|
background-color: #fffbeb;
|
||||||
|
color: #92400e;
|
||||||
|
border: 1px solid #fed7aa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-info {
|
||||||
|
background-color: #eff6ff;
|
||||||
|
color: #1e40af;
|
||||||
|
border: 1px solid #bfdbfe;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard Layout */
|
||||||
|
.dashboard-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background: var(--card-background);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 2rem;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card Styles */
|
||||||
|
.card {
|
||||||
|
background: var(--card-background);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
padding: 1.5rem 2rem;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* System Status Styles */
|
||||||
|
.status-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 1rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon.success {
|
||||||
|
background-color: #dcfce7;
|
||||||
|
color: var(--success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon.error {
|
||||||
|
background-color: #fef2f2;
|
||||||
|
color: var(--error-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon.warning {
|
||||||
|
background-color: #fffbeb;
|
||||||
|
color: var(--warning-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-content h4 {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-content p {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enrollment Form Styles */
|
||||||
|
.enrollment-form {
|
||||||
|
display: grid;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group-full {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Progress Styles */
|
||||||
|
.progress-container {
|
||||||
|
margin-top: 2rem;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background-color: #e5e7eb;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
width: 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-steps {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-step {
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-step.active {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-step.completed {
|
||||||
|
background-color: var(--success-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-step.error {
|
||||||
|
background-color: var(--error-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Log Display Styles */
|
||||||
|
.log-container {
|
||||||
|
background-color: #1f2937;
|
||||||
|
color: #f9fafb;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry {
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry.info {
|
||||||
|
color: #60a5fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry.success {
|
||||||
|
color: #34d399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry.warning {
|
||||||
|
color: #fbbf24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry.error {
|
||||||
|
color: #f87171;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.header {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-steps {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading Spinner */
|
||||||
|
.spinner {
|
||||||
|
display: inline-block;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top-color: #fff;
|
||||||
|
animation: spin 1s ease-in-out infinite;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utility Classes */
|
||||||
|
.text-center { text-align: center; }
|
||||||
|
.text-left { text-align: left; }
|
||||||
|
.text-right { text-align: right; }
|
||||||
|
|
||||||
|
.mt-1 { margin-top: 0.25rem; }
|
||||||
|
.mt-2 { margin-top: 0.5rem; }
|
||||||
|
.mt-3 { margin-top: 0.75rem; }
|
||||||
|
.mt-4 { margin-top: 1rem; }
|
||||||
|
|
||||||
|
.mb-1 { margin-bottom: 0.25rem; }
|
||||||
|
.mb-2 { margin-bottom: 0.5rem; }
|
||||||
|
.mb-3 { margin-bottom: 0.75rem; }
|
||||||
|
.mb-4 { margin-bottom: 1rem; }
|
||||||
|
|
||||||
|
.hidden { display: none; }
|
||||||
|
.block { display: block; }
|
||||||
|
|
||||||
|
.font-bold { font-weight: 700; }
|
||||||
|
.font-medium { font-weight: 500; }
|
||||||
|
|
||||||
|
.text-sm { font-size: 0.875rem; }
|
||||||
|
.text-xs { font-size: 0.75rem; }
|
||||||
|
|
@ -0,0 +1,415 @@
|
||||||
|
/**
|
||||||
|
* ZitiNexus Router Enrollment UI JavaScript
|
||||||
|
*/
|
||||||
|
|
||||||
|
class EnrollmentUI {
|
||||||
|
constructor() {
|
||||||
|
this.enrollmentInProgress = false;
|
||||||
|
this.progressSteps = [
|
||||||
|
'INIT', 'REQUIREMENTS', 'INSTALL', 'DIRECTORIES',
|
||||||
|
'REGISTER', 'CONFIG', 'ENROLL', 'SERVICE', 'START', 'REPORT', 'COMPLETE'
|
||||||
|
];
|
||||||
|
this.currentStep = 0;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.bindEvents();
|
||||||
|
this.loadSystemStatus();
|
||||||
|
|
||||||
|
// Auto-refresh system status every 30 seconds
|
||||||
|
setInterval(() => {
|
||||||
|
if (!this.enrollmentInProgress) {
|
||||||
|
this.loadSystemStatus();
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
// Enrollment form submission
|
||||||
|
const enrollForm = document.getElementById('enrollmentForm');
|
||||||
|
if (enrollForm) {
|
||||||
|
enrollForm.addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.startEnrollment();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash key validation
|
||||||
|
const hashKeyInput = document.getElementById('hashKey');
|
||||||
|
if (hashKeyInput) {
|
||||||
|
hashKeyInput.addEventListener('input', (e) => {
|
||||||
|
this.validateHashKey(e.target);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// API endpoint validation
|
||||||
|
const apiEndpointInput = document.getElementById('apiEndpoint');
|
||||||
|
if (apiEndpointInput) {
|
||||||
|
apiEndpointInput.addEventListener('input', (e) => {
|
||||||
|
this.validateApiEndpoint(e.target);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh system status button
|
||||||
|
const refreshBtn = document.getElementById('refreshStatus');
|
||||||
|
if (refreshBtn) {
|
||||||
|
refreshBtn.addEventListener('click', () => {
|
||||||
|
this.loadSystemStatus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear logs button
|
||||||
|
const clearLogsBtn = document.getElementById('clearLogs');
|
||||||
|
if (clearLogsBtn) {
|
||||||
|
clearLogsBtn.addEventListener('click', () => {
|
||||||
|
this.clearLogs();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validateHashKey(input) {
|
||||||
|
const value = input.value.trim();
|
||||||
|
const isValid = /^[a-fA-F0-9]{32}$/.test(value);
|
||||||
|
|
||||||
|
if (value.length === 0) {
|
||||||
|
this.setInputState(input, 'neutral');
|
||||||
|
} else if (isValid) {
|
||||||
|
this.setInputState(input, 'valid');
|
||||||
|
} else {
|
||||||
|
this.setInputState(input, 'invalid', 'Hash key must be 32 hexadecimal characters');
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
validateApiEndpoint(input) {
|
||||||
|
const value = input.value.trim();
|
||||||
|
const isValid = /^https?:\/\/.+/.test(value);
|
||||||
|
|
||||||
|
if (value.length === 0) {
|
||||||
|
this.setInputState(input, 'neutral');
|
||||||
|
} else if (isValid) {
|
||||||
|
this.setInputState(input, 'valid');
|
||||||
|
} else {
|
||||||
|
this.setInputState(input, 'invalid', 'Must be a valid HTTP/HTTPS URL');
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
setInputState(input, state, message = '') {
|
||||||
|
const formGroup = input.closest('.form-group');
|
||||||
|
const feedback = formGroup.querySelector('.form-feedback') || this.createFeedbackElement(formGroup);
|
||||||
|
|
||||||
|
// Remove existing state classes
|
||||||
|
input.classList.remove('is-valid', 'is-invalid');
|
||||||
|
feedback.classList.remove('valid-feedback', 'invalid-feedback');
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case 'valid':
|
||||||
|
input.classList.add('is-valid');
|
||||||
|
feedback.classList.add('valid-feedback');
|
||||||
|
feedback.textContent = 'Looks good!';
|
||||||
|
feedback.style.display = 'block';
|
||||||
|
break;
|
||||||
|
case 'invalid':
|
||||||
|
input.classList.add('is-invalid');
|
||||||
|
feedback.classList.add('invalid-feedback');
|
||||||
|
feedback.textContent = message;
|
||||||
|
feedback.style.display = 'block';
|
||||||
|
break;
|
||||||
|
case 'neutral':
|
||||||
|
default:
|
||||||
|
feedback.style.display = 'none';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createFeedbackElement(formGroup) {
|
||||||
|
const feedback = document.createElement('div');
|
||||||
|
feedback.className = 'form-feedback';
|
||||||
|
feedback.style.fontSize = '0.875rem';
|
||||||
|
feedback.style.marginTop = '0.25rem';
|
||||||
|
formGroup.appendChild(feedback);
|
||||||
|
return feedback;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadSystemStatus() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('dashboard.php?action=get_status', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
this.updateSystemStatus(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load system status:', error);
|
||||||
|
this.showAlert('Failed to load system status', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSystemStatus(status) {
|
||||||
|
// Update hostname
|
||||||
|
const hostnameElement = document.getElementById('hostname');
|
||||||
|
if (hostnameElement) {
|
||||||
|
hostnameElement.textContent = status.hostname || 'Unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Ziti status
|
||||||
|
const zitiStatusElement = document.getElementById('zitiStatus');
|
||||||
|
const zitiStatusIcon = document.getElementById('zitiStatusIcon');
|
||||||
|
if (zitiStatusElement && zitiStatusIcon) {
|
||||||
|
if (status.ziti_status === 'installed') {
|
||||||
|
zitiStatusElement.textContent = `Installed (${status.ziti_version})`;
|
||||||
|
zitiStatusIcon.className = 'status-icon success';
|
||||||
|
zitiStatusIcon.innerHTML = '✓';
|
||||||
|
} else {
|
||||||
|
zitiStatusElement.textContent = 'Not Installed';
|
||||||
|
zitiStatusIcon.className = 'status-icon error';
|
||||||
|
zitiStatusIcon.innerHTML = '✗';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update service status
|
||||||
|
const serviceStatusElement = document.getElementById('serviceStatus');
|
||||||
|
const serviceStatusIcon = document.getElementById('serviceStatusIcon');
|
||||||
|
if (serviceStatusElement && serviceStatusIcon) {
|
||||||
|
if (status.service_active) {
|
||||||
|
serviceStatusElement.textContent = 'Running';
|
||||||
|
serviceStatusIcon.className = 'status-icon success';
|
||||||
|
serviceStatusIcon.innerHTML = '▶';
|
||||||
|
} else {
|
||||||
|
serviceStatusElement.textContent = 'Stopped';
|
||||||
|
serviceStatusIcon.className = 'status-icon error';
|
||||||
|
serviceStatusIcon.innerHTML = '⏹';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update configuration status
|
||||||
|
const configStatusElement = document.getElementById('configStatus');
|
||||||
|
const configStatusIcon = document.getElementById('configStatusIcon');
|
||||||
|
if (configStatusElement && configStatusIcon) {
|
||||||
|
if (status.config_exists && status.certificates_exist) {
|
||||||
|
configStatusElement.textContent = 'Configured';
|
||||||
|
configStatusIcon.className = 'status-icon success';
|
||||||
|
configStatusIcon.innerHTML = '⚙';
|
||||||
|
} else if (status.config_exists) {
|
||||||
|
configStatusElement.textContent = 'Partial';
|
||||||
|
configStatusIcon.className = 'status-icon warning';
|
||||||
|
configStatusIcon.innerHTML = '⚠';
|
||||||
|
} else {
|
||||||
|
configStatusElement.textContent = 'Not Configured';
|
||||||
|
configStatusIcon.className = 'status-icon error';
|
||||||
|
configStatusIcon.innerHTML = '✗';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async startEnrollment() {
|
||||||
|
if (this.enrollmentInProgress) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashKeyInput = document.getElementById('hashKey');
|
||||||
|
const apiEndpointInput = document.getElementById('apiEndpoint');
|
||||||
|
const enrollBtn = document.getElementById('enrollBtn');
|
||||||
|
|
||||||
|
// Validate inputs
|
||||||
|
const hashKeyValid = this.validateHashKey(hashKeyInput);
|
||||||
|
const apiEndpointValid = this.validateApiEndpoint(apiEndpointInput);
|
||||||
|
|
||||||
|
if (!hashKeyValid || !apiEndpointValid) {
|
||||||
|
this.showAlert('Please fix the validation errors before proceeding', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start enrollment process
|
||||||
|
this.enrollmentInProgress = true;
|
||||||
|
this.currentStep = 0;
|
||||||
|
|
||||||
|
// Update UI
|
||||||
|
enrollBtn.disabled = true;
|
||||||
|
enrollBtn.innerHTML = '<span class="spinner"></span>Starting Enrollment...';
|
||||||
|
|
||||||
|
this.showProgressContainer();
|
||||||
|
this.clearLogs();
|
||||||
|
this.updateProgress(0, 'Initializing...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('action', 'enroll');
|
||||||
|
formData.append('hashKey', hashKeyInput.value.trim());
|
||||||
|
formData.append('apiEndpoint', apiEndpointInput.value.trim());
|
||||||
|
formData.append('csrf_token', document.querySelector('input[name="csrf_token"]').value);
|
||||||
|
|
||||||
|
const response = await fetch('dashboard.php', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
this.updateProgress(100, 'Enrollment completed successfully!');
|
||||||
|
this.addLogEntry('success', `Router '${result.routerName}' enrolled successfully`);
|
||||||
|
this.showAlert(`Router enrollment completed successfully! Router: ${result.routerName}`, 'success');
|
||||||
|
|
||||||
|
// Refresh system status after successful enrollment
|
||||||
|
setTimeout(() => {
|
||||||
|
this.loadSystemStatus();
|
||||||
|
}, 2000);
|
||||||
|
} else {
|
||||||
|
throw new Error(result.error || 'Enrollment failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Enrollment failed:', error);
|
||||||
|
this.updateProgress(null, 'Enrollment failed');
|
||||||
|
this.addLogEntry('error', `Enrollment failed: ${error.message}`);
|
||||||
|
this.showAlert(`Enrollment failed: ${error.message}`, 'error');
|
||||||
|
} finally {
|
||||||
|
this.enrollmentInProgress = false;
|
||||||
|
enrollBtn.disabled = false;
|
||||||
|
enrollBtn.innerHTML = 'Start Enrollment';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showProgressContainer() {
|
||||||
|
const progressContainer = document.getElementById('progressContainer');
|
||||||
|
if (progressContainer) {
|
||||||
|
progressContainer.classList.add('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProgress(percentage, message) {
|
||||||
|
// Update progress bar
|
||||||
|
const progressFill = document.getElementById('progressFill');
|
||||||
|
if (progressFill && percentage !== null) {
|
||||||
|
progressFill.style.width = `${percentage}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update current step
|
||||||
|
if (message) {
|
||||||
|
this.addLogEntry('info', message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update progress steps
|
||||||
|
this.updateProgressSteps();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProgressSteps() {
|
||||||
|
const progressSteps = document.querySelectorAll('.progress-step');
|
||||||
|
progressSteps.forEach((step, index) => {
|
||||||
|
step.classList.remove('active', 'completed', 'error');
|
||||||
|
|
||||||
|
if (index < this.currentStep) {
|
||||||
|
step.classList.add('completed');
|
||||||
|
} else if (index === this.currentStep) {
|
||||||
|
step.classList.add('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addLogEntry(type, message) {
|
||||||
|
const logContainer = document.getElementById('logContainer');
|
||||||
|
if (!logContainer) return;
|
||||||
|
|
||||||
|
const timestamp = new Date().toLocaleTimeString();
|
||||||
|
const logEntry = document.createElement('div');
|
||||||
|
logEntry.className = `log-entry ${type}`;
|
||||||
|
logEntry.textContent = `[${timestamp}] ${message}`;
|
||||||
|
|
||||||
|
logContainer.appendChild(logEntry);
|
||||||
|
logContainer.scrollTop = logContainer.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearLogs() {
|
||||||
|
const logContainer = document.getElementById('logContainer');
|
||||||
|
if (logContainer) {
|
||||||
|
logContainer.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showAlert(message, type = 'info') {
|
||||||
|
// Remove existing alerts
|
||||||
|
const existingAlerts = document.querySelectorAll('.alert');
|
||||||
|
existingAlerts.forEach(alert => alert.remove());
|
||||||
|
|
||||||
|
// Create new alert
|
||||||
|
const alert = document.createElement('div');
|
||||||
|
alert.className = `alert alert-${type}`;
|
||||||
|
alert.textContent = message;
|
||||||
|
|
||||||
|
// Insert at the top of main content
|
||||||
|
const mainContent = document.querySelector('.main-content');
|
||||||
|
if (mainContent) {
|
||||||
|
mainContent.insertBefore(alert, mainContent.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-remove after 5 seconds for non-error alerts
|
||||||
|
if (type !== 'error') {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (alert.parentNode) {
|
||||||
|
alert.remove();
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility method to format file sizes
|
||||||
|
formatFileSize(bytes) {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility method to format timestamps
|
||||||
|
formatTimestamp(timestamp) {
|
||||||
|
return new Date(timestamp * 1000).toLocaleString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the application when DOM is loaded
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
window.enrollmentUI = new EnrollmentUI();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add CSS classes for input validation
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
.form-input.is-valid {
|
||||||
|
border-color: var(--success-color);
|
||||||
|
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input.is-invalid {
|
||||||
|
border-color: var(--error-color);
|
||||||
|
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.valid-feedback {
|
||||||
|
color: var(--success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.invalid-feedback {
|
||||||
|
color: var(--error-color);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
|
@ -0,0 +1,209 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API Client for ZitiNexus Portal communication
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once 'config.php';
|
||||||
|
|
||||||
|
class ApiClient {
|
||||||
|
private $apiEndpoint;
|
||||||
|
private $userAgent;
|
||||||
|
private $timeout;
|
||||||
|
private $maxRetries;
|
||||||
|
|
||||||
|
public function __construct($apiEndpoint = DEFAULT_API_ENDPOINT) {
|
||||||
|
$this->apiEndpoint = rtrim($apiEndpoint, '/');
|
||||||
|
$this->userAgent = 'ZitiRouter-EnrollmentUI/' . APP_VERSION;
|
||||||
|
$this->timeout = 60;
|
||||||
|
$this->maxRetries = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register router with ZitiNexus Portal
|
||||||
|
*/
|
||||||
|
public function registerRouter($hashKey) {
|
||||||
|
$url = $this->apiEndpoint . '/api/router/register';
|
||||||
|
$payload = json_encode(['hashKey' => $hashKey]);
|
||||||
|
|
||||||
|
logMessage('INFO', "Registering router with API: $url");
|
||||||
|
|
||||||
|
$response = $this->makeRequest('POST', $url, $payload);
|
||||||
|
|
||||||
|
if (!$response['success']) {
|
||||||
|
logMessage('ERROR', "API registration failed: " . $response['error']);
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($response['body'], true);
|
||||||
|
|
||||||
|
if (!$data || !isset($data['success']) || !$data['success']) {
|
||||||
|
$errorMsg = isset($data['error']['message']) ? $data['error']['message'] : 'Registration failed';
|
||||||
|
logMessage('ERROR', "Registration failed: $errorMsg");
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'error' => $errorMsg
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
logMessage('SUCCESS', "Router registered successfully");
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'data' => $data['data']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Report enrollment status to portal
|
||||||
|
*/
|
||||||
|
public function reportStatus($callbackUrl, $hashKey, $status, $routerInfo = null, $errorMessage = null) {
|
||||||
|
if (empty($callbackUrl)) {
|
||||||
|
logMessage('WARNING', 'No callback URL provided, skipping status report');
|
||||||
|
return ['success' => true];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix callback URL domain mismatch if needed
|
||||||
|
$fixedCallbackUrl = $this->fixCallbackUrl($callbackUrl);
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'hashKey' => $hashKey,
|
||||||
|
'status' => $status
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($status === 'success' && $routerInfo) {
|
||||||
|
$payload['routerInfo'] = $routerInfo;
|
||||||
|
} elseif ($status === 'failed' && $errorMessage) {
|
||||||
|
$payload['error'] = $errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
logMessage('INFO', "Reporting status '$status' to: $fixedCallbackUrl");
|
||||||
|
|
||||||
|
$response = $this->makeRequest('POST', $fixedCallbackUrl, json_encode($payload));
|
||||||
|
|
||||||
|
if ($response['success']) {
|
||||||
|
logMessage('SUCCESS', 'Status reported successfully');
|
||||||
|
} else {
|
||||||
|
logMessage('WARNING', 'Failed to report status: ' . $response['error']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make HTTP request with retry logic
|
||||||
|
*/
|
||||||
|
private function makeRequest($method, $url, $data = null) {
|
||||||
|
$retryCount = 0;
|
||||||
|
|
||||||
|
while ($retryCount < $this->maxRetries) {
|
||||||
|
$ch = curl_init();
|
||||||
|
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_URL => $url,
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_TIMEOUT => $this->timeout,
|
||||||
|
CURLOPT_CONNECTTIMEOUT => 30,
|
||||||
|
CURLOPT_USERAGENT => $this->userAgent,
|
||||||
|
CURLOPT_HTTPHEADER => [
|
||||||
|
'Content-Type: application/json',
|
||||||
|
'Accept: application/json'
|
||||||
|
],
|
||||||
|
CURLOPT_SSL_VERIFYPEER => true,
|
||||||
|
CURLOPT_SSL_VERIFYHOST => 2,
|
||||||
|
CURLOPT_FOLLOWLOCATION => true,
|
||||||
|
CURLOPT_MAXREDIRS => 3
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($method === 'POST' && $data) {
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
$error = curl_error($ch);
|
||||||
|
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($response === false) {
|
||||||
|
logMessage('ERROR', "cURL error: $error");
|
||||||
|
$retryCount++;
|
||||||
|
if ($retryCount < $this->maxRetries) {
|
||||||
|
$waitTime = $retryCount * 2;
|
||||||
|
logMessage('INFO', "Retrying in {$waitTime}s... (attempt $retryCount/$this->maxRetries)");
|
||||||
|
sleep($waitTime);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($httpCode === 200) {
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'body' => $response,
|
||||||
|
'http_code' => $httpCode
|
||||||
|
];
|
||||||
|
} elseif ($httpCode === 429) {
|
||||||
|
// Rate limited
|
||||||
|
$retryCount++;
|
||||||
|
if ($retryCount < $this->maxRetries) {
|
||||||
|
$waitTime = $retryCount * 2;
|
||||||
|
logMessage('WARNING', "Rate limited. Waiting {$waitTime}s before retry $retryCount/$this->maxRetries");
|
||||||
|
sleep($waitTime);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
$errorMsg = "HTTP $httpCode";
|
||||||
|
if ($response) {
|
||||||
|
$responseData = json_decode($response, true);
|
||||||
|
if ($responseData && isset($responseData['error']['message'])) {
|
||||||
|
$errorMsg .= ': ' . $responseData['error']['message'];
|
||||||
|
} elseif ($responseData && isset($responseData['message'])) {
|
||||||
|
$errorMsg .= ': ' . $responseData['message'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'error' => $errorMsg,
|
||||||
|
'http_code' => $httpCode,
|
||||||
|
'body' => $response
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Max retries exceeded',
|
||||||
|
'http_code' => 0
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix callback URL domain mismatch
|
||||||
|
*/
|
||||||
|
private function fixCallbackUrl($callbackUrl) {
|
||||||
|
// Replace api.zitinexus.com with backend.zitinexus.com to match script's API endpoint
|
||||||
|
if (strpos($callbackUrl, 'api.zitinexus.com') !== false) {
|
||||||
|
$fixedUrl = str_replace('api.zitinexus.com', 'backend.zitinexus.com', $callbackUrl);
|
||||||
|
logMessage('INFO', "Fixed callback URL domain: $fixedUrl");
|
||||||
|
return $fixedUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $callbackUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate hash key format
|
||||||
|
*/
|
||||||
|
public static function validateHashKey($hashKey) {
|
||||||
|
return preg_match('/^[a-fA-F0-9]{32}$/', $hashKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate API endpoint format
|
||||||
|
*/
|
||||||
|
public static function validateApiEndpoint($endpoint) {
|
||||||
|
return filter_var($endpoint, FILTER_VALIDATE_URL) &&
|
||||||
|
(strpos($endpoint, 'http://') === 0 || strpos($endpoint, 'https://') === 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Authentication handler for Ziti Router Enrollment UI
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once 'config.php';
|
||||||
|
|
||||||
|
class AuthManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticate user with username and password
|
||||||
|
*/
|
||||||
|
public static function authenticate($username, $password) {
|
||||||
|
$username = sanitizeInput($username);
|
||||||
|
|
||||||
|
if ($username === ADMIN_USERNAME && password_verify($password, ADMIN_PASSWORD_HASH)) {
|
||||||
|
$_SESSION['authenticated'] = true;
|
||||||
|
$_SESSION['username'] = $username;
|
||||||
|
$_SESSION['last_activity'] = time();
|
||||||
|
$_SESSION['login_time'] = time();
|
||||||
|
|
||||||
|
// Generate new CSRF token
|
||||||
|
generateCSRFToken();
|
||||||
|
|
||||||
|
logMessage('INFO', "User '$username' logged in successfully from " . $_SERVER['REMOTE_ADDR']);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
logMessage('WARNING', "Failed login attempt for user '$username' from " . $_SERVER['REMOTE_ADDR']);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logout user
|
||||||
|
*/
|
||||||
|
public static function logout() {
|
||||||
|
if (isset($_SESSION['username'])) {
|
||||||
|
logMessage('INFO', "User '{$_SESSION['username']}' logged out");
|
||||||
|
}
|
||||||
|
|
||||||
|
session_destroy();
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user is authenticated and session is valid
|
||||||
|
*/
|
||||||
|
public static function requireAuth() {
|
||||||
|
if (!isAuthenticated() || !isSessionValid()) {
|
||||||
|
header('Location: index.php?error=session_expired');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current user info
|
||||||
|
*/
|
||||||
|
public static function getCurrentUser() {
|
||||||
|
if (!isAuthenticated()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'username' => $_SESSION['username'] ?? '',
|
||||||
|
'login_time' => $_SESSION['login_time'] ?? 0,
|
||||||
|
'last_activity' => $_SESSION['last_activity'] ?? 0
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check CSRF token for forms
|
||||||
|
*/
|
||||||
|
public static function requireCSRF() {
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$token = $_POST[CSRF_TOKEN_NAME] ?? '';
|
||||||
|
if (!verifyCSRFToken($token)) {
|
||||||
|
http_response_code(403);
|
||||||
|
die('CSRF token validation failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle login form submission
|
||||||
|
*/
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'login') {
|
||||||
|
$username = $_POST['username'] ?? '';
|
||||||
|
$password = $_POST['password'] ?? '';
|
||||||
|
|
||||||
|
if (AuthManager::authenticate($username, $password)) {
|
||||||
|
header('Location: dashboard.php');
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
$loginError = 'Invalid username or password';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle logout
|
||||||
|
*/
|
||||||
|
if (isset($_GET['action']) && $_GET['action'] === 'logout') {
|
||||||
|
AuthManager::logout();
|
||||||
|
header('Location: index.php?message=logged_out');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
@ -0,0 +1,147 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Configuration file for Ziti Router Enrollment UI
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Start session if not already started
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Application configuration
|
||||||
|
define('APP_NAME', 'ZitiNexus Router Enrollment');
|
||||||
|
define('APP_VERSION', '1.0.0');
|
||||||
|
|
||||||
|
// Default API configuration
|
||||||
|
define('DEFAULT_API_ENDPOINT', 'https://backend.zitinexus.com');
|
||||||
|
|
||||||
|
// File paths (matching the bash script)
|
||||||
|
define('CONFIG_DIR', '/etc/zitirouter');
|
||||||
|
define('CERTS_DIR', CONFIG_DIR . '/certs');
|
||||||
|
define('ROUTER_CONFIG', CONFIG_DIR . '/router.yaml');
|
||||||
|
define('JWT_FILE', CONFIG_DIR . '/enrollment.jwt');
|
||||||
|
define('LOG_FILE', '/var/log/ziti-router-enrollment.log');
|
||||||
|
define('SYSTEMD_SERVICE_FILE', '/etc/systemd/system/ziti-router.service');
|
||||||
|
|
||||||
|
// UI specific paths
|
||||||
|
define('UI_LOG_DIR', __DIR__ . '/../logs');
|
||||||
|
define('UI_TEMP_DIR', __DIR__ . '/../temp');
|
||||||
|
|
||||||
|
// Authentication
|
||||||
|
define('ADMIN_USERNAME', 'admin');
|
||||||
|
define('ADMIN_PASSWORD_HASH', password_hash('admin123', PASSWORD_DEFAULT)); // Change this in production
|
||||||
|
|
||||||
|
// Security settings
|
||||||
|
define('SESSION_TIMEOUT', 3600); // 1 hour
|
||||||
|
define('CSRF_TOKEN_NAME', 'csrf_token');
|
||||||
|
|
||||||
|
// System commands
|
||||||
|
define('SYSTEMCTL_CMD', 'systemctl');
|
||||||
|
define('HOSTNAME_CMD', 'hostname');
|
||||||
|
define('ZITI_CMD', 'ziti');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user is authenticated
|
||||||
|
*/
|
||||||
|
function isAuthenticated() {
|
||||||
|
return isset($_SESSION['authenticated']) && $_SESSION['authenticated'] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if session is valid
|
||||||
|
*/
|
||||||
|
function isSessionValid() {
|
||||||
|
if (!isset($_SESSION['last_activity'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (time() - $_SESSION['last_activity'] > SESSION_TIMEOUT) {
|
||||||
|
session_destroy();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$_SESSION['last_activity'] = time();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate CSRF token
|
||||||
|
*/
|
||||||
|
function generateCSRFToken() {
|
||||||
|
if (!isset($_SESSION[CSRF_TOKEN_NAME])) {
|
||||||
|
$_SESSION[CSRF_TOKEN_NAME] = bin2hex(random_bytes(32));
|
||||||
|
}
|
||||||
|
return $_SESSION[CSRF_TOKEN_NAME];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify CSRF token
|
||||||
|
*/
|
||||||
|
function verifyCSRFToken($token) {
|
||||||
|
return isset($_SESSION[CSRF_TOKEN_NAME]) && hash_equals($_SESSION[CSRF_TOKEN_NAME], $token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize input
|
||||||
|
*/
|
||||||
|
function sanitizeInput($input) {
|
||||||
|
return htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log message to file
|
||||||
|
*/
|
||||||
|
function logMessage($level, $message) {
|
||||||
|
$timestamp = date('Y-m-d H:i:s');
|
||||||
|
$logEntry = "[$timestamp] [$level] $message" . PHP_EOL;
|
||||||
|
|
||||||
|
// Try to write to system log first
|
||||||
|
if (is_writable(dirname(LOG_FILE))) {
|
||||||
|
file_put_contents(LOG_FILE, $logEntry, FILE_APPEND | LOCK_EX);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also write to UI log
|
||||||
|
$uiLogFile = UI_LOG_DIR . '/ui-enrollment.log';
|
||||||
|
if (!is_dir(UI_LOG_DIR)) {
|
||||||
|
mkdir(UI_LOG_DIR, 0755, true);
|
||||||
|
}
|
||||||
|
file_put_contents($uiLogFile, $logEntry, FILE_APPEND | LOCK_EX);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if running as root/admin
|
||||||
|
*/
|
||||||
|
function isRunningAsRoot() {
|
||||||
|
return posix_getuid() === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute system command safely
|
||||||
|
*/
|
||||||
|
function executeCommand($command, &$output = null, &$returnCode = null) {
|
||||||
|
$descriptorspec = [
|
||||||
|
0 => ['pipe', 'r'], // stdin
|
||||||
|
1 => ['pipe', 'w'], // stdout
|
||||||
|
2 => ['pipe', 'w'] // stderr
|
||||||
|
];
|
||||||
|
|
||||||
|
$process = proc_open($command, $descriptorspec, $pipes);
|
||||||
|
|
||||||
|
if (is_resource($process)) {
|
||||||
|
fclose($pipes[0]);
|
||||||
|
|
||||||
|
$stdout = stream_get_contents($pipes[1]);
|
||||||
|
$stderr = stream_get_contents($pipes[2]);
|
||||||
|
|
||||||
|
fclose($pipes[1]);
|
||||||
|
fclose($pipes[2]);
|
||||||
|
|
||||||
|
$returnCode = proc_close($process);
|
||||||
|
$output = trim($stdout . $stderr);
|
||||||
|
|
||||||
|
return $returnCode === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
@ -0,0 +1,539 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Enrollment Manager for Ziti Router
|
||||||
|
* Replicates the functionality of the bash script
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once 'config.php';
|
||||||
|
require_once 'api_client.php';
|
||||||
|
|
||||||
|
class EnrollmentManager {
|
||||||
|
private $apiClient;
|
||||||
|
private $routerData;
|
||||||
|
private $progressCallback;
|
||||||
|
|
||||||
|
public function __construct($apiEndpoint = DEFAULT_API_ENDPOINT) {
|
||||||
|
$this->apiClient = new ApiClient($apiEndpoint);
|
||||||
|
$this->routerData = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set progress callback function
|
||||||
|
*/
|
||||||
|
public function setProgressCallback($callback) {
|
||||||
|
$this->progressCallback = $callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Report progress
|
||||||
|
*/
|
||||||
|
private function reportProgress($step, $message, $percentage = null) {
|
||||||
|
logMessage('INFO', "[$step] $message");
|
||||||
|
|
||||||
|
if ($this->progressCallback && is_callable($this->progressCallback)) {
|
||||||
|
call_user_func($this->progressCallback, $step, $message, $percentage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main enrollment process
|
||||||
|
*/
|
||||||
|
public function enrollRouter($hashKey, $apiEndpoint = null) {
|
||||||
|
try {
|
||||||
|
if ($apiEndpoint) {
|
||||||
|
$this->apiClient = new ApiClient($apiEndpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->reportProgress('INIT', 'Starting router enrollment process...', 0);
|
||||||
|
|
||||||
|
// Step 1: Check system requirements
|
||||||
|
$this->reportProgress('REQUIREMENTS', 'Checking system requirements...', 10);
|
||||||
|
if (!$this->checkSystemRequirements()) {
|
||||||
|
throw new Exception('System requirements check failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Install OpenZiti if needed
|
||||||
|
$this->reportProgress('INSTALL', 'Installing OpenZiti CLI...', 20);
|
||||||
|
if (!$this->installZiti()) {
|
||||||
|
throw new Exception('OpenZiti installation failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Create directories
|
||||||
|
$this->reportProgress('DIRECTORIES', 'Creating necessary directories...', 30);
|
||||||
|
if (!$this->createDirectories()) {
|
||||||
|
throw new Exception('Failed to create directories');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Register router with API
|
||||||
|
$this->reportProgress('REGISTER', 'Registering router with ZitiNexus Portal...', 40);
|
||||||
|
$result = $this->apiClient->registerRouter($hashKey);
|
||||||
|
if (!$result['success']) {
|
||||||
|
throw new Exception('Router registration failed: ' . $result['error']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->routerData = $result['data'];
|
||||||
|
$this->reportProgress('REGISTER', 'Router registered successfully: ' . $this->routerData['routerInfo']['name'], 50);
|
||||||
|
|
||||||
|
// Step 5: Save configuration files
|
||||||
|
$this->reportProgress('CONFIG', 'Saving configuration files...', 60);
|
||||||
|
if (!$this->saveConfiguration()) {
|
||||||
|
throw new Exception('Failed to save configuration files');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 6: Enroll router with OpenZiti
|
||||||
|
$this->reportProgress('ENROLL', 'Enrolling router with OpenZiti controller...', 70);
|
||||||
|
if (!$this->enrollWithZiti()) {
|
||||||
|
throw new Exception('Router enrollment with OpenZiti failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 7: Create systemd service
|
||||||
|
$this->reportProgress('SERVICE', 'Creating systemd service...', 80);
|
||||||
|
if (!$this->createSystemdService()) {
|
||||||
|
throw new Exception('Failed to create systemd service');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 8: Start router service
|
||||||
|
$this->reportProgress('START', 'Starting router service...', 90);
|
||||||
|
if (!$this->startRouter()) {
|
||||||
|
throw new Exception('Failed to start router service');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 9: Report success status
|
||||||
|
$this->reportProgress('REPORT', 'Reporting enrollment status...', 95);
|
||||||
|
$this->reportSuccessStatus($hashKey);
|
||||||
|
|
||||||
|
$this->reportProgress('COMPLETE', 'Router enrollment completed successfully!', 100);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'routerName' => $this->routerData['routerInfo']['name'],
|
||||||
|
'routerId' => $this->routerData['routerInfo']['id'],
|
||||||
|
'message' => 'Router enrollment completed successfully'
|
||||||
|
];
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$errorMsg = $e->getMessage();
|
||||||
|
logMessage('ERROR', $errorMsg);
|
||||||
|
$this->reportProgress('ERROR', $errorMsg, null);
|
||||||
|
|
||||||
|
// Report failure status
|
||||||
|
if (!empty($hashKey) && !empty($this->routerData['callbackUrl'])) {
|
||||||
|
$this->apiClient->reportStatus(
|
||||||
|
$this->routerData['callbackUrl'],
|
||||||
|
$hashKey,
|
||||||
|
'failed',
|
||||||
|
null,
|
||||||
|
$errorMsg
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'error' => $errorMsg
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check system requirements
|
||||||
|
*/
|
||||||
|
private function checkSystemRequirements() {
|
||||||
|
// Check if running as root
|
||||||
|
if (!isRunningAsRoot()) {
|
||||||
|
throw new Exception('This script must be run as root (use sudo)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if curl is available
|
||||||
|
if (!$this->checkCommand('curl')) {
|
||||||
|
$this->reportProgress('REQUIREMENTS', 'Installing curl...');
|
||||||
|
if (!$this->installPackage('curl')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if jq is available
|
||||||
|
if (!$this->checkCommand('jq')) {
|
||||||
|
$this->reportProgress('REQUIREMENTS', 'Installing jq...');
|
||||||
|
if (!$this->installPackage('jq')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if systemctl is available
|
||||||
|
if (!$this->checkCommand('systemctl')) {
|
||||||
|
throw new Exception('systemctl is required but not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install OpenZiti CLI
|
||||||
|
*/
|
||||||
|
private function installZiti() {
|
||||||
|
// Check if ziti is already installed
|
||||||
|
if ($this->checkCommand('ziti')) {
|
||||||
|
$output = '';
|
||||||
|
executeCommand('ziti version 2>/dev/null | head -n1', $output);
|
||||||
|
$this->reportProgress('INSTALL', 'OpenZiti CLI already installed: ' . trim($output));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->reportProgress('INSTALL', 'Setting up OpenZiti package repository...');
|
||||||
|
|
||||||
|
// Add GPG key
|
||||||
|
$gpgCommand = 'curl -sSLf https://get.openziti.io/tun/package-repos.gpg | gpg --dearmor --output /usr/share/keyrings/openziti.gpg';
|
||||||
|
if (!executeCommand($gpgCommand)) {
|
||||||
|
throw new Exception('Failed to add OpenZiti GPG key');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set proper permissions
|
||||||
|
if (!executeCommand('chmod a+r /usr/share/keyrings/openziti.gpg')) {
|
||||||
|
throw new Exception('Failed to set GPG key permissions');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add repository to sources list
|
||||||
|
$repoContent = 'deb [signed-by=/usr/share/keyrings/openziti.gpg] https://packages.openziti.org/zitipax-openziti-deb-stable debian main';
|
||||||
|
if (!file_put_contents('/etc/apt/sources.list.d/openziti-release.list', $repoContent)) {
|
||||||
|
throw new Exception('Failed to add OpenZiti repository');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update package list
|
||||||
|
$this->reportProgress('INSTALL', 'Updating package list...');
|
||||||
|
if (!executeCommand('apt-get update')) {
|
||||||
|
throw new Exception('Failed to update package list');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install openziti-router package
|
||||||
|
$this->reportProgress('INSTALL', 'Installing openziti-router package...');
|
||||||
|
if (!executeCommand('apt-get install -y openziti-router')) {
|
||||||
|
$this->reportProgress('INSTALL', 'Trying to install ziti CLI only...');
|
||||||
|
if (!executeCommand('apt-get install -y ziti')) {
|
||||||
|
throw new Exception('Failed to install OpenZiti CLI');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify installation
|
||||||
|
if (!$this->checkCommand('ziti')) {
|
||||||
|
throw new Exception('OpenZiti CLI installation failed - command not found after installation');
|
||||||
|
}
|
||||||
|
|
||||||
|
$output = '';
|
||||||
|
executeCommand('ziti version 2>/dev/null | head -n1', $output);
|
||||||
|
$this->reportProgress('INSTALL', 'OpenZiti CLI installed successfully: ' . trim($output));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create necessary directories
|
||||||
|
*/
|
||||||
|
private function createDirectories() {
|
||||||
|
$directories = [
|
||||||
|
CONFIG_DIR => 0755,
|
||||||
|
CERTS_DIR => 0700,
|
||||||
|
dirname(LOG_FILE) => 0755
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($directories as $dir => $permissions) {
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
if (!mkdir($dir, $permissions, true)) {
|
||||||
|
throw new Exception("Failed to create directory: $dir");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chmod($dir, $permissions);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save configuration files
|
||||||
|
*/
|
||||||
|
private function saveConfiguration() {
|
||||||
|
// Save JWT
|
||||||
|
if (!file_put_contents(JWT_FILE, $this->routerData['jwt'])) {
|
||||||
|
throw new Exception('Failed to save JWT file');
|
||||||
|
}
|
||||||
|
chmod(JWT_FILE, 0600);
|
||||||
|
|
||||||
|
// Save router configuration
|
||||||
|
if (!file_put_contents(ROUTER_CONFIG, $this->routerData['routerConfig']['yaml'])) {
|
||||||
|
throw new Exception('Failed to save router configuration');
|
||||||
|
}
|
||||||
|
chmod(ROUTER_CONFIG, 0644);
|
||||||
|
|
||||||
|
// Fix router configuration for proper enrollment
|
||||||
|
$this->fixRouterConfiguration();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix router configuration (replicate bash script logic)
|
||||||
|
*/
|
||||||
|
private function fixRouterConfiguration() {
|
||||||
|
// Create backup
|
||||||
|
copy(ROUTER_CONFIG, ROUTER_CONFIG . '.backup');
|
||||||
|
|
||||||
|
$routerName = $this->routerData['routerInfo']['name'];
|
||||||
|
$routerId = $this->routerData['routerInfo']['id'];
|
||||||
|
$tenantId = $this->routerData['metadata']['tenantId'];
|
||||||
|
$controllerEndpoint = $this->routerData['metadata']['controllerEndpoint'] ?? 'enroll.zitinexus.com:443';
|
||||||
|
|
||||||
|
// Add tls: prefix if not present
|
||||||
|
if (strpos($controllerEndpoint, 'tls:') !== 0) {
|
||||||
|
$controllerEndpoint = 'tls:' . $controllerEndpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build role attributes
|
||||||
|
$roleAttributesSection = '# No role attributes specified';
|
||||||
|
if (!empty($this->routerData['routerInfo']['roleAttributes'])) {
|
||||||
|
$roleAttributesSection = "roleAttributes:";
|
||||||
|
foreach ($this->routerData['routerInfo']['roleAttributes'] as $attr) {
|
||||||
|
$roleAttributesSection .= "\n - \"$attr\"";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$generatedAt = date('c');
|
||||||
|
|
||||||
|
$configContent = <<<EOF
|
||||||
|
v: 3
|
||||||
|
|
||||||
|
identity:
|
||||||
|
cert: /etc/zitirouter/certs/$routerName.cert
|
||||||
|
server_cert: /etc/zitirouter/certs/$routerName.server.chain.cert
|
||||||
|
key: /etc/zitirouter/certs/$routerName.key
|
||||||
|
ca: /etc/zitirouter/certs/$routerName.cas
|
||||||
|
|
||||||
|
ctrl:
|
||||||
|
endpoint: $controllerEndpoint
|
||||||
|
|
||||||
|
link:
|
||||||
|
dialers:
|
||||||
|
- binding: transport
|
||||||
|
|
||||||
|
listeners:
|
||||||
|
# bindings of edge and tunnel requires an "edge" section below
|
||||||
|
- binding: edge
|
||||||
|
address: tls:0.0.0.0:443
|
||||||
|
options:
|
||||||
|
advertise: 127.0.0.1:443
|
||||||
|
connectTimeoutMs: 5000
|
||||||
|
getSessionTimeout: 60
|
||||||
|
- binding: tunnel
|
||||||
|
options:
|
||||||
|
mode: host
|
||||||
|
|
||||||
|
edge:
|
||||||
|
csr:
|
||||||
|
country: SG
|
||||||
|
province: SG
|
||||||
|
locality: Singapore
|
||||||
|
organization: Genworx
|
||||||
|
organizationalUnit: ZitiNexus
|
||||||
|
sans:
|
||||||
|
dns:
|
||||||
|
- localhost
|
||||||
|
- $routerName
|
||||||
|
ip:
|
||||||
|
- "127.0.0.1"
|
||||||
|
- "::1"
|
||||||
|
|
||||||
|
# Tenant-specific role attributes
|
||||||
|
$roleAttributesSection
|
||||||
|
|
||||||
|
# Router metadata
|
||||||
|
metadata:
|
||||||
|
tenantId: "$tenantId"
|
||||||
|
zitiRouterId: "$routerId"
|
||||||
|
routerType: "private-edge"
|
||||||
|
generatedAt: "$generatedAt"
|
||||||
|
generatedBy: "ZitiNexus"
|
||||||
|
EOF;
|
||||||
|
|
||||||
|
file_put_contents(ROUTER_CONFIG, $configContent);
|
||||||
|
chmod(ROUTER_CONFIG, 0644);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enroll router with OpenZiti
|
||||||
|
*/
|
||||||
|
private function enrollWithZiti() {
|
||||||
|
$command = 'ziti router enroll --jwt ' . JWT_FILE . ' ' . ROUTER_CONFIG . ' 2>&1';
|
||||||
|
$output = '';
|
||||||
|
|
||||||
|
if (!executeCommand($command, $output)) {
|
||||||
|
throw new Exception('Router enrollment failed: ' . $output);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify certificates were created
|
||||||
|
$routerName = $this->routerData['routerInfo']['name'];
|
||||||
|
$certFile = CERTS_DIR . '/' . $routerName . '.cert';
|
||||||
|
|
||||||
|
if (!file_exists($certFile)) {
|
||||||
|
throw new Exception('Router certificate not found after enrollment');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create systemd service
|
||||||
|
*/
|
||||||
|
private function createSystemdService() {
|
||||||
|
$finalConfig = '/etc/zitirouter/zitirouter.yaml';
|
||||||
|
|
||||||
|
// Copy router config to final location
|
||||||
|
if (!copy(ROUTER_CONFIG, $finalConfig)) {
|
||||||
|
throw new Exception('Failed to copy router config to final location');
|
||||||
|
}
|
||||||
|
chmod($finalConfig, 0644);
|
||||||
|
|
||||||
|
$serviceContent = <<<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=OpenZiti Router
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/bin/ziti router run /etc/zitirouter/zitirouter.yaml
|
||||||
|
Restart=on-failure
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=/etc/zitirouter
|
||||||
|
LimitNOFILE=65536
|
||||||
|
StandardOutput=append:/var/log/ziti-router.log
|
||||||
|
StandardError=append:/var/log/ziti-router.log
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF;
|
||||||
|
|
||||||
|
if (!file_put_contents(SYSTEMD_SERVICE_FILE, $serviceContent)) {
|
||||||
|
throw new Exception('Failed to create systemd service file');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload systemd and enable service
|
||||||
|
if (!executeCommand('systemctl daemon-reload')) {
|
||||||
|
throw new Exception('Failed to reload systemd');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!executeCommand('systemctl enable ziti-router.service')) {
|
||||||
|
throw new Exception('Failed to enable ziti-router service');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start router service
|
||||||
|
*/
|
||||||
|
private function startRouter() {
|
||||||
|
if (!executeCommand('systemctl start ziti-router.service')) {
|
||||||
|
throw new Exception('Failed to start router service');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait and check status
|
||||||
|
sleep(3);
|
||||||
|
$output = '';
|
||||||
|
executeCommand('systemctl is-active ziti-router.service', $output);
|
||||||
|
|
||||||
|
if (trim($output) !== 'active') {
|
||||||
|
logMessage('WARNING', 'Router service may not be running properly');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Report success status
|
||||||
|
*/
|
||||||
|
private function reportSuccessStatus($hashKey) {
|
||||||
|
if (empty($this->routerData['callbackUrl'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$hostname = '';
|
||||||
|
$arch = '';
|
||||||
|
$os = '';
|
||||||
|
$zitiVersion = '';
|
||||||
|
|
||||||
|
executeCommand('hostname', $hostname);
|
||||||
|
executeCommand('uname -m', $arch);
|
||||||
|
executeCommand('lsb_release -d 2>/dev/null | cut -f2', $os);
|
||||||
|
executeCommand('ziti version 2>/dev/null | head -n1', $zitiVersion);
|
||||||
|
|
||||||
|
$routerInfo = [
|
||||||
|
'version' => trim($zitiVersion) ?: 'unknown',
|
||||||
|
'hostname' => trim($hostname),
|
||||||
|
'arch' => trim($arch),
|
||||||
|
'os' => trim($os) ?: 'Linux'
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->apiClient->reportStatus(
|
||||||
|
$this->routerData['callbackUrl'],
|
||||||
|
$hashKey,
|
||||||
|
'success',
|
||||||
|
$routerInfo
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if command exists
|
||||||
|
*/
|
||||||
|
private function checkCommand($command) {
|
||||||
|
$output = '';
|
||||||
|
return executeCommand("which $command", $output);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install package using apt
|
||||||
|
*/
|
||||||
|
private function installPackage($package) {
|
||||||
|
return executeCommand("apt-get update && apt-get install -y $package");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get system status information
|
||||||
|
*/
|
||||||
|
public function getSystemStatus() {
|
||||||
|
$status = [
|
||||||
|
'hostname' => '',
|
||||||
|
'ziti_status' => 'unknown',
|
||||||
|
'ziti_version' => '',
|
||||||
|
'service_active' => false,
|
||||||
|
'config_exists' => false,
|
||||||
|
'certificates_exist' => false
|
||||||
|
];
|
||||||
|
|
||||||
|
// Get hostname
|
||||||
|
executeCommand('hostname', $status['hostname']);
|
||||||
|
$status['hostname'] = trim($status['hostname']);
|
||||||
|
|
||||||
|
// Check if ziti command exists and get version
|
||||||
|
if ($this->checkCommand('ziti')) {
|
||||||
|
executeCommand('ziti version 2>/dev/null | head -n1', $status['ziti_version']);
|
||||||
|
$status['ziti_version'] = trim($status['ziti_version']);
|
||||||
|
$status['ziti_status'] = 'installed';
|
||||||
|
} else {
|
||||||
|
$status['ziti_status'] = 'not_installed';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check service status
|
||||||
|
$output = '';
|
||||||
|
if (executeCommand('systemctl is-active ziti-router.service 2>/dev/null', $output)) {
|
||||||
|
$status['service_active'] = trim($output) === 'active';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check configuration files
|
||||||
|
$status['config_exists'] = file_exists(ROUTER_CONFIG);
|
||||||
|
|
||||||
|
// Check certificates
|
||||||
|
if (is_dir(CERTS_DIR)) {
|
||||||
|
$certFiles = glob(CERTS_DIR . '/*.cert');
|
||||||
|
$status['certificates_exist'] = !empty($certFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
@ -0,0 +1,399 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# ZitiNexus Router Enrollment UI Installation Script
|
||||||
|
# For Ubuntu 22.04/24.04 LTS
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
WEB_DIR="/var/www/ziti-enrollment"
|
||||||
|
DOMAIN="ziti-enrollment.local"
|
||||||
|
WEB_USER="www-data"
|
||||||
|
|
||||||
|
# Logging function
|
||||||
|
log() {
|
||||||
|
local level=$1
|
||||||
|
shift
|
||||||
|
local message="$*"
|
||||||
|
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
case $level in
|
||||||
|
"ERROR")
|
||||||
|
echo -e "${RED}[ERROR]${NC} $message" >&2
|
||||||
|
;;
|
||||||
|
"SUCCESS")
|
||||||
|
echo -e "${GREEN}[SUCCESS]${NC} $message"
|
||||||
|
;;
|
||||||
|
"WARNING")
|
||||||
|
echo -e "${YELLOW}[WARNING]${NC} $message"
|
||||||
|
;;
|
||||||
|
"INFO")
|
||||||
|
echo -e "${BLUE}[INFO]${NC} $message"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "$message"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Error handling
|
||||||
|
error_exit() {
|
||||||
|
log "ERROR" "$1"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if running as root
|
||||||
|
check_root() {
|
||||||
|
if [[ $EUID -ne 0 ]]; then
|
||||||
|
error_exit "This script must be run as root (use sudo)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Detect web server preference
|
||||||
|
detect_web_server() {
|
||||||
|
echo
|
||||||
|
log "INFO" "Which web server would you like to use?"
|
||||||
|
echo "1) Apache (recommended)"
|
||||||
|
echo "2) Nginx"
|
||||||
|
read -p "Enter your choice (1-2): " choice
|
||||||
|
|
||||||
|
case $choice in
|
||||||
|
1)
|
||||||
|
WEB_SERVER="apache"
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
WEB_SERVER="nginx"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
log "WARNING" "Invalid choice, defaulting to Apache"
|
||||||
|
WEB_SERVER="apache"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
log "INFO" "Selected web server: $WEB_SERVER"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Install web server and PHP
|
||||||
|
install_web_server() {
|
||||||
|
log "INFO" "Installing web server and PHP..."
|
||||||
|
|
||||||
|
# Update package list
|
||||||
|
apt update || error_exit "Failed to update package list"
|
||||||
|
|
||||||
|
if [[ "$WEB_SERVER" == "apache" ]]; then
|
||||||
|
# Install Apache and PHP
|
||||||
|
apt install -y apache2 php8.1 php8.1-curl php8.1-json libapache2-mod-php8.1 || error_exit "Failed to install Apache and PHP"
|
||||||
|
|
||||||
|
# Enable and start Apache
|
||||||
|
systemctl enable apache2 || error_exit "Failed to enable Apache"
|
||||||
|
systemctl start apache2 || error_exit "Failed to start Apache"
|
||||||
|
|
||||||
|
log "SUCCESS" "Apache and PHP installed successfully"
|
||||||
|
|
||||||
|
elif [[ "$WEB_SERVER" == "nginx" ]]; then
|
||||||
|
# Install Nginx and PHP-FPM
|
||||||
|
apt install -y nginx php8.1-fpm php8.1-curl php8.1-json || error_exit "Failed to install Nginx and PHP"
|
||||||
|
|
||||||
|
# Enable and start services
|
||||||
|
systemctl enable nginx php8.1-fpm || error_exit "Failed to enable Nginx and PHP-FPM"
|
||||||
|
systemctl start nginx php8.1-fpm || error_exit "Failed to start Nginx and PHP-FPM"
|
||||||
|
|
||||||
|
log "SUCCESS" "Nginx and PHP-FPM installed successfully"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Deploy UI files
|
||||||
|
deploy_ui() {
|
||||||
|
log "INFO" "Deploying UI files..."
|
||||||
|
|
||||||
|
# Create web directory
|
||||||
|
mkdir -p "$WEB_DIR" || error_exit "Failed to create web directory"
|
||||||
|
|
||||||
|
# Copy UI files (assuming script is run from the UI directory)
|
||||||
|
if [[ -d "public" && -d "includes" && -d "assets" ]]; then
|
||||||
|
cp -r public includes assets logs temp README.md "$WEB_DIR/" || error_exit "Failed to copy UI files"
|
||||||
|
else
|
||||||
|
error_exit "UI files not found. Please run this script from the UI directory."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set proper permissions
|
||||||
|
chown -R "$WEB_USER:$WEB_USER" "$WEB_DIR" || error_exit "Failed to set ownership"
|
||||||
|
chmod -R 755 "$WEB_DIR" || error_exit "Failed to set permissions"
|
||||||
|
chmod -R 777 "$WEB_DIR/logs" "$WEB_DIR/temp" || error_exit "Failed to set log/temp permissions"
|
||||||
|
|
||||||
|
log "SUCCESS" "UI files deployed successfully"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Configure Apache
|
||||||
|
configure_apache() {
|
||||||
|
log "INFO" "Configuring Apache virtual host..."
|
||||||
|
|
||||||
|
# Create virtual host configuration
|
||||||
|
cat > "/etc/apache2/sites-available/ziti-enrollment.conf" << EOF
|
||||||
|
<VirtualHost *:80>
|
||||||
|
ServerName $DOMAIN
|
||||||
|
DocumentRoot $WEB_DIR/public
|
||||||
|
|
||||||
|
<Directory $WEB_DIR/public>
|
||||||
|
AllowOverride All
|
||||||
|
Require all granted
|
||||||
|
DirectoryIndex index.php
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
ErrorLog \${APACHE_LOG_DIR}/ziti-enrollment_error.log
|
||||||
|
CustomLog \${APACHE_LOG_DIR}/ziti-enrollment_access.log combined
|
||||||
|
</VirtualHost>
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Enable site and modules
|
||||||
|
a2ensite ziti-enrollment.conf || error_exit "Failed to enable site"
|
||||||
|
a2enmod rewrite || error_exit "Failed to enable rewrite module"
|
||||||
|
|
||||||
|
# Disable default site
|
||||||
|
a2dissite 000-default.conf || log "WARNING" "Failed to disable default site"
|
||||||
|
|
||||||
|
# Reload Apache
|
||||||
|
systemctl reload apache2 || error_exit "Failed to reload Apache"
|
||||||
|
|
||||||
|
log "SUCCESS" "Apache configured successfully"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Configure Nginx
|
||||||
|
configure_nginx() {
|
||||||
|
log "INFO" "Configuring Nginx virtual host..."
|
||||||
|
|
||||||
|
# Create virtual host configuration
|
||||||
|
cat > "/etc/nginx/sites-available/ziti-enrollment" << EOF
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name $DOMAIN;
|
||||||
|
root $WEB_DIR/public;
|
||||||
|
index index.php;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files \$uri \$uri/ /index.php?\$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ \.php$ {
|
||||||
|
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
|
||||||
|
fastcgi_index index.php;
|
||||||
|
fastcgi_param SCRIPT_FILENAME \$realpath_root\$fastcgi_script_name;
|
||||||
|
include fastcgi_params;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ /\. {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Enable site
|
||||||
|
ln -sf "/etc/nginx/sites-available/ziti-enrollment" "/etc/nginx/sites-enabled/" || error_exit "Failed to enable site"
|
||||||
|
|
||||||
|
# Remove default site
|
||||||
|
rm -f "/etc/nginx/sites-enabled/default" || log "WARNING" "Failed to remove default site"
|
||||||
|
|
||||||
|
# Test and reload Nginx
|
||||||
|
nginx -t || error_exit "Nginx configuration test failed"
|
||||||
|
systemctl reload nginx || error_exit "Failed to reload Nginx"
|
||||||
|
|
||||||
|
log "SUCCESS" "Nginx configured successfully"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Configure PHP
|
||||||
|
configure_php() {
|
||||||
|
log "INFO" "Configuring PHP..."
|
||||||
|
|
||||||
|
# Find PHP configuration file
|
||||||
|
if [[ "$WEB_SERVER" == "apache" ]]; then
|
||||||
|
PHP_INI="/etc/php/8.1/apache2/php.ini"
|
||||||
|
else
|
||||||
|
PHP_INI="/etc/php/8.1/fpm/php.ini"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if exec functions are disabled
|
||||||
|
if grep -q "disable_functions.*exec" "$PHP_INI"; then
|
||||||
|
log "WARNING" "PHP exec functions may be disabled. Please check $PHP_INI"
|
||||||
|
log "INFO" "Ensure exec, shell_exec, proc_open are NOT in disable_functions"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Restart web server to apply PHP changes
|
||||||
|
if [[ "$WEB_SERVER" == "apache" ]]; then
|
||||||
|
systemctl restart apache2 || error_exit "Failed to restart Apache"
|
||||||
|
else
|
||||||
|
systemctl restart php8.1-fpm || error_exit "Failed to restart PHP-FPM"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "SUCCESS" "PHP configured successfully"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set up sudo access
|
||||||
|
setup_sudo() {
|
||||||
|
log "INFO" "Setting up sudo access for web server..."
|
||||||
|
|
||||||
|
# Create sudoers file
|
||||||
|
cat > "/etc/sudoers.d/ziti-enrollment" << 'EOF'
|
||||||
|
# Allow www-data to run system commands for Ziti enrollment
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/apt-get
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/systemctl
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/mkdir
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/chmod
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/chown
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/curl
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/gpg
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/ziti
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/which
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/hostname
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/uname
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/lsb_release
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Validate sudoers file
|
||||||
|
if visudo -c -f "/etc/sudoers.d/ziti-enrollment"; then
|
||||||
|
log "SUCCESS" "Sudo access configured successfully"
|
||||||
|
else
|
||||||
|
error_exit "Invalid sudoers configuration"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update hosts file
|
||||||
|
update_hosts() {
|
||||||
|
log "INFO" "Updating hosts file..."
|
||||||
|
|
||||||
|
# Check if entry already exists
|
||||||
|
if ! grep -q "$DOMAIN" /etc/hosts; then
|
||||||
|
echo "127.0.0.1 $DOMAIN" >> /etc/hosts
|
||||||
|
log "SUCCESS" "Added $DOMAIN to hosts file"
|
||||||
|
else
|
||||||
|
log "INFO" "Domain already exists in hosts file"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test installation
|
||||||
|
test_installation() {
|
||||||
|
log "INFO" "Testing installation..."
|
||||||
|
|
||||||
|
# Test web server
|
||||||
|
if [[ "$WEB_SERVER" == "apache" ]]; then
|
||||||
|
if systemctl is-active --quiet apache2; then
|
||||||
|
log "SUCCESS" "Apache is running"
|
||||||
|
else
|
||||||
|
log "ERROR" "Apache is not running"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if systemctl is-active --quiet nginx && systemctl is-active --quiet php8.1-fpm; then
|
||||||
|
log "SUCCESS" "Nginx and PHP-FPM are running"
|
||||||
|
else
|
||||||
|
log "ERROR" "Nginx or PHP-FPM is not running"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test PHP
|
||||||
|
if php -v > /dev/null 2>&1; then
|
||||||
|
log "SUCCESS" "PHP is working"
|
||||||
|
else
|
||||||
|
log "ERROR" "PHP is not working"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test sudo access
|
||||||
|
if sudo -u www-data sudo -n systemctl --version > /dev/null 2>&1; then
|
||||||
|
log "SUCCESS" "Sudo access is working"
|
||||||
|
else
|
||||||
|
log "WARNING" "Sudo access may not be working properly"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test file permissions
|
||||||
|
if [[ -r "$WEB_DIR/public/index.php" ]]; then
|
||||||
|
log "SUCCESS" "File permissions are correct"
|
||||||
|
else
|
||||||
|
log "ERROR" "File permissions may be incorrect"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Show final information
|
||||||
|
show_final_info() {
|
||||||
|
echo
|
||||||
|
echo "=============================================="
|
||||||
|
echo " INSTALLATION COMPLETED"
|
||||||
|
echo "=============================================="
|
||||||
|
echo
|
||||||
|
log "SUCCESS" "ZitiNexus Router Enrollment UI installed successfully!"
|
||||||
|
echo
|
||||||
|
echo "Access Information:"
|
||||||
|
echo " URL: http://$DOMAIN"
|
||||||
|
echo " Username: admin"
|
||||||
|
echo " Password: admin123"
|
||||||
|
echo
|
||||||
|
echo "Important Notes:"
|
||||||
|
echo " 1. Change the default password in production"
|
||||||
|
echo " 2. Consider setting up HTTPS for production use"
|
||||||
|
echo " 3. Review security settings in $WEB_DIR/includes/config.php"
|
||||||
|
echo
|
||||||
|
echo "File Locations:"
|
||||||
|
echo " Web Directory: $WEB_DIR"
|
||||||
|
echo " Configuration: $WEB_DIR/includes/config.php"
|
||||||
|
echo " Logs: $WEB_DIR/logs/"
|
||||||
|
echo
|
||||||
|
echo "Useful Commands:"
|
||||||
|
if [[ "$WEB_SERVER" == "apache" ]]; then
|
||||||
|
echo " Check status: systemctl status apache2"
|
||||||
|
echo " View logs: tail -f /var/log/apache2/ziti-enrollment_error.log"
|
||||||
|
else
|
||||||
|
echo " Check status: systemctl status nginx php8.1-fpm"
|
||||||
|
echo " View logs: tail -f /var/log/nginx/error.log"
|
||||||
|
fi
|
||||||
|
echo " Test sudo: sudo -u www-data sudo -l"
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main installation function
|
||||||
|
main() {
|
||||||
|
echo "=============================================="
|
||||||
|
echo " ZitiNexus Router Enrollment UI Installer"
|
||||||
|
echo "=============================================="
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Check if running as root
|
||||||
|
check_root
|
||||||
|
|
||||||
|
# Detect web server preference
|
||||||
|
detect_web_server
|
||||||
|
|
||||||
|
# Install web server and PHP
|
||||||
|
install_web_server
|
||||||
|
|
||||||
|
# Deploy UI files
|
||||||
|
deploy_ui
|
||||||
|
|
||||||
|
# Configure web server
|
||||||
|
if [[ "$WEB_SERVER" == "apache" ]]; then
|
||||||
|
configure_apache
|
||||||
|
else
|
||||||
|
configure_nginx
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Configure PHP
|
||||||
|
configure_php
|
||||||
|
|
||||||
|
# Set up sudo access
|
||||||
|
setup_sudo
|
||||||
|
|
||||||
|
# Update hosts file
|
||||||
|
update_hosts
|
||||||
|
|
||||||
|
# Test installation
|
||||||
|
test_installation
|
||||||
|
|
||||||
|
# Show final information
|
||||||
|
show_final_info
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run main function
|
||||||
|
main "$@"
|
||||||
|
|
@ -0,0 +1,310 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Main dashboard for ZitiNexus Router Enrollment UI
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once '../includes/auth.php';
|
||||||
|
require_once '../includes/enrollment.php';
|
||||||
|
|
||||||
|
// Require authentication
|
||||||
|
AuthManager::requireAuth();
|
||||||
|
|
||||||
|
// Get current user
|
||||||
|
$currentUser = AuthManager::getCurrentUser();
|
||||||
|
|
||||||
|
// Initialize enrollment manager
|
||||||
|
$enrollmentManager = new EnrollmentManager();
|
||||||
|
|
||||||
|
// Handle AJAX requests
|
||||||
|
if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if (isset($_GET['action']) && $_GET['action'] === 'get_status') {
|
||||||
|
// Get system status
|
||||||
|
$status = $enrollmentManager->getSystemStatus();
|
||||||
|
echo json_encode($status);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'enroll') {
|
||||||
|
// Handle enrollment request
|
||||||
|
AuthManager::requireCSRF();
|
||||||
|
|
||||||
|
$hashKey = sanitizeInput($_POST['hashKey'] ?? '');
|
||||||
|
$apiEndpoint = sanitizeInput($_POST['apiEndpoint'] ?? DEFAULT_API_ENDPOINT);
|
||||||
|
|
||||||
|
// Validate inputs
|
||||||
|
if (!ApiClient::validateHashKey($hashKey)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Invalid hash key format']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ApiClient::validateApiEndpoint($apiEndpoint)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Invalid API endpoint format']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start enrollment
|
||||||
|
$result = $enrollmentManager->enrollRouter($hashKey, $apiEndpoint);
|
||||||
|
echo json_encode($result);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get system status for initial page load
|
||||||
|
$systemStatus = $enrollmentManager->getSystemStatus();
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><?php echo APP_NAME; ?> - Dashboard</title>
|
||||||
|
<link rel="stylesheet" href="../assets/css/style.css">
|
||||||
|
<link rel="icon" type="image/x-icon" href="../assets/images/favicon.ico">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="dashboard-container">
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="header">
|
||||||
|
<div class="header-content">
|
||||||
|
<h1 class="header-title"><?php echo APP_NAME; ?></h1>
|
||||||
|
<div class="header-actions">
|
||||||
|
<div class="user-info">
|
||||||
|
Welcome, <strong><?php echo htmlspecialchars($currentUser['username']); ?></strong>
|
||||||
|
<span class="text-xs">
|
||||||
|
| Logged in: <?php echo date('M j, Y g:i A', $currentUser['login_time']); ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button id="refreshStatus" class="btn btn-secondary" title="Refresh System Status">
|
||||||
|
🔄 Refresh
|
||||||
|
</button>
|
||||||
|
<a href="index.php?action=logout" class="btn btn-secondary">
|
||||||
|
Logout
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="main-content">
|
||||||
|
<!-- System Status Card -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2 class="card-title">System Status</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="status-grid">
|
||||||
|
<div class="status-item">
|
||||||
|
<div id="zitiStatusIcon" class="status-icon <?php echo $systemStatus['ziti_status'] === 'installed' ? 'success' : 'error'; ?>">
|
||||||
|
<?php echo $systemStatus['ziti_status'] === 'installed' ? '✓' : '✗'; ?>
|
||||||
|
</div>
|
||||||
|
<div class="status-content">
|
||||||
|
<h4>Ziti CLI Status</h4>
|
||||||
|
<p id="zitiStatus">
|
||||||
|
<?php
|
||||||
|
if ($systemStatus['ziti_status'] === 'installed') {
|
||||||
|
echo 'Installed (' . htmlspecialchars($systemStatus['ziti_version']) . ')';
|
||||||
|
} else {
|
||||||
|
echo 'Not Installed';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-item">
|
||||||
|
<div id="serviceStatusIcon" class="status-icon <?php echo $systemStatus['service_active'] ? 'success' : 'error'; ?>">
|
||||||
|
<?php echo $systemStatus['service_active'] ? '▶' : '⏹'; ?>
|
||||||
|
</div>
|
||||||
|
<div class="status-content">
|
||||||
|
<h4>Router Service</h4>
|
||||||
|
<p id="serviceStatus">
|
||||||
|
<?php echo $systemStatus['service_active'] ? 'Running' : 'Stopped'; ?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-item">
|
||||||
|
<div id="configStatusIcon" class="status-icon <?php
|
||||||
|
if ($systemStatus['config_exists'] && $systemStatus['certificates_exist']) {
|
||||||
|
echo 'success';
|
||||||
|
} elseif ($systemStatus['config_exists']) {
|
||||||
|
echo 'warning';
|
||||||
|
} else {
|
||||||
|
echo 'error';
|
||||||
|
}
|
||||||
|
?>">
|
||||||
|
<?php
|
||||||
|
if ($systemStatus['config_exists'] && $systemStatus['certificates_exist']) {
|
||||||
|
echo '⚙';
|
||||||
|
} elseif ($systemStatus['config_exists']) {
|
||||||
|
echo '⚠';
|
||||||
|
} else {
|
||||||
|
echo '✗';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
<div class="status-content">
|
||||||
|
<h4>Configuration</h4>
|
||||||
|
<p id="configStatus">
|
||||||
|
<?php
|
||||||
|
if ($systemStatus['config_exists'] && $systemStatus['certificates_exist']) {
|
||||||
|
echo 'Configured';
|
||||||
|
} elseif ($systemStatus['config_exists']) {
|
||||||
|
echo 'Partial';
|
||||||
|
} else {
|
||||||
|
echo 'Not Configured';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-item">
|
||||||
|
<div class="status-icon success">
|
||||||
|
🖥
|
||||||
|
</div>
|
||||||
|
<div class="status-content">
|
||||||
|
<h4>Hostname</h4>
|
||||||
|
<p id="hostname"><?php echo htmlspecialchars($systemStatus['hostname']); ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Enrollment Form Card -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2 class="card-title">Router Enrollment</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form id="enrollmentForm" class="enrollment-form">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?php echo generateCSRFToken(); ?>">
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="apiEndpoint" class="form-label">API Endpoint</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
id="apiEndpoint"
|
||||||
|
name="apiEndpoint"
|
||||||
|
class="form-input"
|
||||||
|
value="<?php echo DEFAULT_API_ENDPOINT; ?>"
|
||||||
|
placeholder="https://backend.zitinexus.com"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<small class="text-sm text-secondary">ZitiNexus Portal API endpoint</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="hashKey" class="form-label">Hash Key</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="hashKey"
|
||||||
|
name="hashKey"
|
||||||
|
class="form-input"
|
||||||
|
placeholder="32-character hexadecimal hash key"
|
||||||
|
maxlength="32"
|
||||||
|
pattern="[a-fA-F0-9]{32}"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<small class="text-sm text-secondary">Router enrollment hash key from ZitiNexus Portal</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group-full">
|
||||||
|
<button type="submit" id="enrollBtn" class="btn btn-primary">
|
||||||
|
Start Enrollment
|
||||||
|
</button>
|
||||||
|
<button type="button" id="clearLogs" class="btn btn-secondary" style="margin-left: 1rem;">
|
||||||
|
Clear Logs
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Progress Container -->
|
||||||
|
<div id="progressContainer" class="progress-container">
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div id="progressFill" class="progress-fill"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="progress-steps">
|
||||||
|
<div class="progress-step">Initialize</div>
|
||||||
|
<div class="progress-step">Requirements</div>
|
||||||
|
<div class="progress-step">Install</div>
|
||||||
|
<div class="progress-step">Directories</div>
|
||||||
|
<div class="progress-step">Register</div>
|
||||||
|
<div class="progress-step">Configure</div>
|
||||||
|
<div class="progress-step">Enroll</div>
|
||||||
|
<div class="progress-step">Service</div>
|
||||||
|
<div class="progress-step">Start</div>
|
||||||
|
<div class="progress-step">Report</div>
|
||||||
|
<div class="progress-step">Complete</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="logContainer" class="log-container">
|
||||||
|
<!-- Log entries will be added here dynamically -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Information Card -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2 class="card-title">Information</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 2rem;">
|
||||||
|
<div>
|
||||||
|
<h4 class="font-medium mb-2">How to Use</h4>
|
||||||
|
<ol class="text-sm text-secondary" style="padding-left: 1.5rem;">
|
||||||
|
<li>Obtain a hash key from the ZitiNexus Portal by creating a router enrollment</li>
|
||||||
|
<li>Enter the API endpoint (default is pre-filled)</li>
|
||||||
|
<li>Paste the 32-character hash key</li>
|
||||||
|
<li>Click "Start Enrollment" to begin the process</li>
|
||||||
|
<li>Monitor the progress and logs for status updates</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 class="font-medium mb-2">System Requirements</h4>
|
||||||
|
<ul class="text-sm text-secondary" style="padding-left: 1.5rem;">
|
||||||
|
<li>Ubuntu 22.04 or 24.04 LTS</li>
|
||||||
|
<li>Root/sudo access required</li>
|
||||||
|
<li>Internet connectivity</li>
|
||||||
|
<li>systemctl available</li>
|
||||||
|
<li>curl and jq packages (auto-installed)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 class="font-medium mb-2">File Locations</h4>
|
||||||
|
<ul class="text-sm text-secondary" style="padding-left: 1.5rem;">
|
||||||
|
<li><code>/etc/zitirouter/</code> - Configuration directory</li>
|
||||||
|
<li><code>/etc/zitirouter/certs/</code> - Certificates</li>
|
||||||
|
<li><code>/var/log/ziti-router-enrollment.log</code> - Enrollment log</li>
|
||||||
|
<li><code>/etc/systemd/system/ziti-router.service</code> - Service file</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 class="font-medium mb-2">Service Management</h4>
|
||||||
|
<ul class="text-sm text-secondary" style="padding-left: 1.5rem;">
|
||||||
|
<li><code>systemctl status ziti-router</code> - Check status</li>
|
||||||
|
<li><code>systemctl start ziti-router</code> - Start service</li>
|
||||||
|
<li><code>systemctl stop ziti-router</code> - Stop service</li>
|
||||||
|
<li><code>journalctl -u ziti-router -f</code> - View logs</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="../assets/js/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,135 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Login page for ZitiNexus Router Enrollment UI
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once '../includes/auth.php';
|
||||||
|
|
||||||
|
// Redirect if already authenticated
|
||||||
|
if (isAuthenticated() && isSessionValid()) {
|
||||||
|
header('Location: dashboard.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle messages
|
||||||
|
$message = '';
|
||||||
|
$messageType = '';
|
||||||
|
|
||||||
|
if (isset($_GET['error'])) {
|
||||||
|
switch ($_GET['error']) {
|
||||||
|
case 'session_expired':
|
||||||
|
$message = 'Your session has expired. Please log in again.';
|
||||||
|
$messageType = 'warning';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$message = 'An error occurred. Please try again.';
|
||||||
|
$messageType = 'error';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($_GET['message'])) {
|
||||||
|
switch ($_GET['message']) {
|
||||||
|
case 'logged_out':
|
||||||
|
$message = 'You have been logged out successfully.';
|
||||||
|
$messageType = 'info';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($loginError)) {
|
||||||
|
$message = $loginError;
|
||||||
|
$messageType = 'error';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><?php echo APP_NAME; ?> - Login</title>
|
||||||
|
<link rel="stylesheet" href="../assets/css/style.css">
|
||||||
|
<link rel="icon" type="image/x-icon" href="../assets/images/favicon.ico">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="login-card">
|
||||||
|
<div class="login-header">
|
||||||
|
<h1><?php echo APP_NAME; ?></h1>
|
||||||
|
<p>Router Enrollment Management Interface</p>
|
||||||
|
<p class="text-sm text-secondary">Version <?php echo APP_VERSION; ?></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($message): ?>
|
||||||
|
<div class="alert alert-<?php echo $messageType; ?>">
|
||||||
|
<?php echo htmlspecialchars($message); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form method="POST" action="index.php">
|
||||||
|
<input type="hidden" name="action" value="login">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username" class="form-label">Username</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="username"
|
||||||
|
name="username"
|
||||||
|
class="form-input"
|
||||||
|
required
|
||||||
|
autocomplete="username"
|
||||||
|
value="<?php echo isset($_POST['username']) ? htmlspecialchars($_POST['username']) : ''; ?>"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password" class="form-label">Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
class="form-input"
|
||||||
|
required
|
||||||
|
autocomplete="current-password"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary btn-full">
|
||||||
|
Sign In
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div style="margin-top: 2rem; padding-top: 1rem; border-top: 1px solid var(--border-color); text-align: center;">
|
||||||
|
<p class="text-sm text-secondary">
|
||||||
|
Default credentials: <strong>admin</strong> / <strong>admin123</strong>
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-secondary" style="margin-top: 0.5rem;">
|
||||||
|
Please change the default password in production
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Auto-focus on username field
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const usernameField = document.getElementById('username');
|
||||||
|
if (usernameField && !usernameField.value) {
|
||||||
|
usernameField.focus();
|
||||||
|
} else {
|
||||||
|
const passwordField = document.getElementById('password');
|
||||||
|
if (passwordField) {
|
||||||
|
passwordField.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle form submission
|
||||||
|
document.querySelector('form').addEventListener('submit', function(e) {
|
||||||
|
const submitBtn = this.querySelector('button[type="submit"]');
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.innerHTML = '<span class="spinner"></span>Signing In...';
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue