Files
amneziavpnphp/DEVELOPER.md
T
infosave2007 1f91f17f57 Add multilingual support with translations for German, Russian, French, and Chinese
Added time limits and backup functions for servers
2025-11-08 09:14:20 +03:00

13 KiB

Developer Guide

Guide for developers contributing to Amnezia VPN Web Panel.

Development Setup

Local Development (without Docker)

  1. 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
  1. Install MySQL 8.0
# Ubuntu/Debian
sudo apt install mysql-server-8.0

# macOS
brew install mysql@8.0
  1. Install Composer
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer
  1. Clone and Setup
git clone <repo-url>
cd amnezia-web-panel
composer install
  1. 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;
  1. Update Database Config

Edit inc/DB.php:

private static $config = [
    'host' => 'localhost',  // Change from 'db'
    'dbname' => 'amnezia_panel',
    'user' => 'amnezia',
    'password' => 'amnezia123',
    'charset' => 'utf8mb4',
];
  1. Run Development Server
cd public
php -S localhost:8000

Access: http://localhost:8000

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

  1. Fork repository
  2. Create feature branch: git checkout -b feature/my-feature
  3. Make changes
  4. Write tests
  5. Commit: git commit -am 'Add my feature'
  6. Push: git push origin feature/my-feature
  7. Create Pull Request

Commit Message Format

type: subject

body (optional)

footer (optional)

Types:

  • feat: New feature
  • fix: Bug fix
  • docs: Documentation
  • style: Formatting
  • refactor: Code restructuring
  • test: Tests
  • chore: Maintenance

Example:

feat: add server statistics dashboard

- Added stats collection via SSH
- Created stats API endpoint
- Built statistics template
- Updated navigation

Closes #123

Resources


Happy coding! 🚀