Added time limits and backup functions for servers
13 KiB
Developer Guide
Guide for developers contributing to Amnezia VPN Web Panel.
Development Setup
Local Development (without Docker)
- Install PHP 8.2+
# Ubuntu/Debian
sudo apt install php8.2 php8.2-cli php8.2-mysql php8.2-gd php8.2-curl php8.2-mbstring
# macOS (Homebrew)
brew install php@8.2
- Install MySQL 8.0
# Ubuntu/Debian
sudo apt install mysql-server-8.0
# macOS
brew install mysql@8.0
- Install Composer
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer
- Clone and Setup
git clone <repo-url>
cd amnezia-web-panel
composer install
- Configure Database
mysql -u root -p
CREATE DATABASE amnezia_panel CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'amnezia'@'localhost' IDENTIFIED BY 'amnezia123';
GRANT ALL PRIVILEGES ON amnezia_panel.* TO 'amnezia'@'localhost';
FLUSH PRIVILEGES;
USE amnezia_panel;
SOURCE migrations/001_init.sql;
SOURCE migrations/002_translations_ru.sql;
SOURCE migrations/003_translations_es.sql;
SOURCE migrations/004_translations_de.sql;
SOURCE migrations/005_translations_fr.sql;
SOURCE migrations/006_translations_zh.sql;
- Update Database Config
Edit inc/DB.php:
private static $config = [
'host' => 'localhost', // Change from 'db'
'dbname' => 'amnezia_panel',
'user' => 'amnezia',
'password' => 'amnezia123',
'charset' => 'utf8mb4',
];
- Run Development Server
cd public
php -S localhost:8000
Access: http://localhost:8000
Docker Development (Recommended)
docker compose up -d
Access: http://localhost:8082
Live code editing: Mount project as volume (already configured in docker-compose.yml)
Project Architecture
MVC Pattern
Request → Router → Controller Logic → Model → Database
↓
View (Twig) → Response
Core Components
1. Router (inc/Router.php)
Simple pattern-matching router:
Router::get('/path/{param}', function($params) {
// Handler logic
echo $params['param'];
});
Router::post('/form', function() {
// Handle POST
$data = $_POST['field'];
});
2. Database (inc/DB.php)
Singleton PDO connection:
$pdo = DB::conn();
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$id]);
$user = $stmt->fetch();
3. Authentication (inc/Auth.php)
Session-based auth:
// Login
Auth::login($email, $password);
// Get current user
$user = Auth::user();
// Check admin
if (Auth::isAdmin()) {
// Admin logic
}
// Logout
Auth::logout();
// Middleware
requireAuth(); // In route handler
4. Views (inc/View.php)
Twig template rendering:
View::render('template.twig', [
'var1' => 'value1',
'var2' => 'value2',
]);
5. Models
VpnServer (inc/VpnServer.php):
// Create and deploy server
$serverId = VpnServer::create($userId, $name, $host, $port, $username, $password);
// Get server instance
$server = new VpnServer($serverId);
$data = $server->getData();
// Deploy to remote server
$server->deploy();
// List servers
$servers = VpnServer::listAll();
$userServers = VpnServer::listByUser($userId);
VpnClient (inc/VpnClient.php):
// Create client
$clientId = VpnClient::create($serverId, $userId, $name);
// Get client instance
$client = new VpnClient($clientId);
$config = $client->getConfig();
$qrCode = $client->getQRCode();
// List clients
$clients = VpnClient::listByServer($serverId);
$userClients = VpnClient::listByUser($userId);
6. QR Code Utility (inc/QrUtil.php)
Amnezia-compatible QR encoding:
require_once 'inc/QrUtil.php';
// From WireGuard config text
$payload = QrUtil::encodeOldPayloadFromConf($configText);
// Generate PNG data URI
$qrImage = QrUtil::pngBase64($payload);
// Use in template
echo '<img src="' . $qrImage . '">';
Adding New Features
Example: Add Server Statistics
1. Add database column
Create migration migrations/002_add_stats.sql:
ALTER TABLE vpn_servers ADD COLUMN stats_json TEXT;
2. Add method to model
Edit inc/VpnServer.php:
public function getStats(): array {
if (!$this->data['stats_json']) {
return [];
}
return json_decode($this->data['stats_json'], true);
}
public function updateStats(): void {
$stats = $this->collectStatsFromServer();
$pdo = DB::conn();
$stmt = $pdo->prepare('UPDATE vpn_servers SET stats_json = ? WHERE id = ?');
$stmt->execute([json_encode($stats), $this->serverId]);
}
private function collectStatsFromServer(): array {
// SSH to server, get stats
// ...
return ['cpu' => 45, 'memory' => 60, 'bandwidth' => 1024];
}
3. Add route
Edit public/index.php:
Router::get('/servers/{id}/stats', function($params) {
requireAuth();
$serverId = (int)$params['id'];
$server = new VpnServer($serverId);
$stats = $server->getStats();
header('Content-Type: application/json');
echo json_encode($stats);
});
4. Add template
Create templates/servers/stats.twig:
{% extends "layout.twig" %}
{% block content %}
<div class="max-w-4xl mx-auto">
<h1>Server Statistics</h1>
<div class="grid grid-cols-3 gap-4">
<div class="bg-white p-4 rounded shadow">
<h3>CPU Usage</h3>
<p class="text-3xl">{{ stats.cpu }}%</p>
</div>
<!-- More stats -->
</div>
</div>
{% endblock %}
5. Update navigation
Edit templates/layout.twig:
<a href="/servers/{{ server.id }}/stats">Statistics</a>
Code Style Guidelines
PHP
Follow PSR-12 coding standard:
<?php
namespace MyNamespace;
use AnotherNamespace\SomeClass;
class MyClass
{
private string $property;
public function __construct(string $param)
{
$this->property = $param;
}
public function method(int $arg): bool
{
if ($arg > 0) {
return true;
}
return false;
}
}
SQL
-- Use uppercase keywords
SELECT id, name, created_at
FROM vpn_servers
WHERE status = 'active'
ORDER BY created_at DESC;
-- Prepared statements always
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?');
$stmt->execute([$email]);
JavaScript
// Use modern ES6+
const fetchData = async () => {
try {
const response = await fetch('/api/servers');
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Error:', error);
}
};
// Event listeners
document.getElementById('btn').addEventListener('click', () => {
fetchData();
});
Twig
{# Comments #}
{# Variables #}
{{ variable }}
{{ object.property }}
{{ array[0] }}
{# Control structures #}
{% if condition %}
Content
{% endif %}
{% for item in items %}
{{ item.name }}
{% endfor %}
{# Filters #}
{{ text|upper }}
{{ html|raw }} {# Careful with XSS! #}
Security Best Practices
1. SQL Injection Prevention
❌ Never do this:
$sql = "SELECT * FROM users WHERE email = '$email'";
✅ Always use prepared statements:
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?');
$stmt->execute([$email]);
2. XSS Prevention
❌ Never output unescaped:
echo $_GET['name']; // Dangerous!
✅ Escape output:
echo htmlspecialchars($_GET['name'], ENT_QUOTES, 'UTF-8');
In Twig (auto-escapes by default):
{{ user_input }} {# Safe #}
{{ user_input|raw }} {# Unsafe - use carefully #}
3. CSRF Protection
TODO: Implement token-based CSRF protection:
// Generate token
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
// In form
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
// Verify
if ($_POST['csrf_token'] !== $_SESSION['csrf_token']) {
die('CSRF token mismatch');
}
4. Password Hashing
✅ Always use bcrypt:
// Hash
$hash = password_hash($password, PASSWORD_BCRYPT);
// Verify
if (password_verify($password, $hash)) {
// Correct
}
5. Input Validation
// Email
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new Exception('Invalid email');
}
// Integer
$id = (int)$_GET['id'];
// String length
if (strlen($name) < 3 || strlen($name) > 50) {
throw new Exception('Invalid name length');
}
Testing
Unit Tests (TODO)
// tests/VpnServerTest.php
use PHPUnit\Framework\TestCase;
class VpnServerTest extends TestCase
{
public function testCreate()
{
$serverId = VpnServer::create(1, 'Test', '192.168.1.1', 22, 'root', 'pass');
$this->assertIsInt($serverId);
$this->assertGreaterThan(0, $serverId);
}
}
Run tests:
composer require --dev phpunit/phpunit
./vendor/bin/phpunit tests/
Manual Testing
See TESTING.md for comprehensive testing guide.
Debugging
Enable Error Display
In development, edit public/index.php:
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
Database Queries
// Enable query logging
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
try {
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
} catch (PDOException $e) {
error_log("SQL Error: " . $e->getMessage());
error_log("Query: $sql");
error_log("Params: " . print_r($params, true));
throw $e;
}
SSH Commands
// Add debug output
$cmd = "your command";
error_log("Executing SSH command: $cmd");
$output = shell_exec($sshCmd);
error_log("SSH output: $output");
Docker Logs
# Web container logs
docker compose logs -f web
# Database logs
docker compose logs -f db
# Last 100 lines
docker compose logs --tail=100 web
API Development
Adding New Endpoint
// In public/index.php
Router::post('/api/clients', function() {
// TODO: Verify JWT token
header('Content-Type: application/json');
try {
$serverId = (int)$_POST['server_id'];
$name = trim($_POST['name'] ?? '');
if (!$serverId || !$name) {
http_response_code(400);
echo json_encode(['error' => 'Missing parameters']);
return;
}
$user = Auth::user();
$clientId = VpnClient::create($serverId, $user['id'], $name);
$client = new VpnClient($clientId);
echo json_encode([
'success' => true,
'client' => $client->getData(),
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
});
JWT Authentication (TODO)
use Firebase\JWT\JWT;
// Generate token
$payload = [
'user_id' => $user['id'],
'exp' => time() + 3600, // 1 hour
];
$token = JWT::encode($payload, $secretKey, 'HS256');
// Verify token
try {
$decoded = JWT::decode($token, $secretKey, ['HS256']);
$userId = $decoded->user_id;
} catch (Exception $e) {
http_response_code(401);
echo json_encode(['error' => 'Invalid token']);
exit;
}
Deployment
Production Checklist
- Change default admin password
- Update database passwords in docker-compose.yml
- Set up HTTPS (nginx reverse proxy + Let's Encrypt)
- Disable error display
- Enable error logging
- Set up automated backups
- Configure firewall
- Set up monitoring
- Review security settings
- Test disaster recovery
Environment Variables
Create .env.production:
DB_HOST=db
DB_NAME=amnezia_panel
DB_USER=amnezia
DB_PASS=strong_random_password_here
JWT_SECRET=another_strong_random_secret_here
ADMIN_EMAIL=admin@yourdomain.com
Load in PHP:
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
$dbPassword = $_ENV['DB_PASS'];
Contributing
- Fork repository
- Create feature branch:
git checkout -b feature/my-feature - Make changes
- Write tests
- Commit:
git commit -am 'Add my feature' - Push:
git push origin feature/my-feature - Create Pull Request
Commit Message Format
type: subject
body (optional)
footer (optional)
Types:
feat: New featurefix: Bug fixdocs: Documentationstyle: Formattingrefactor: Code restructuringtest: Testschore: Maintenance
Example:
feat: add server statistics dashboard
- Added stats collection via SSH
- Created stats API endpoint
- Built statistics template
- Updated navigation
Closes #123
Resources
- PHP Documentation
- MySQL Reference
- Twig Documentation
- Tailwind CSS
- Docker Documentation
- WireGuard Protocol
- Amnezia VPN GitHub
Happy coding! 🚀