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="https://backend.zitinexus.com"
|
||||
|
||||
|
||||
# Initialize variables to prevent unbound variable errors
|
||||
CALLBACK_URL=""
|
||||
JWT=""
|
||||
|
|
@ -37,7 +38,9 @@ CONTROLLER_ENDPOINT=""
|
|||
ROLE_ATTRIBUTES=""
|
||||
HASH_KEY=""
|
||||
API_ENDPOINT=""
|
||||
|
||||
# Example modification (Automated installation , uncomment below)
|
||||
#API_ENDPOINT="https://backend.zitinexus.com"
|
||||
#HASH_KEY="your-hash-key-here"
|
||||
# Logging function
|
||||
log() {
|
||||
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