Add project files
This commit is contained in:
+691
@@ -0,0 +1,691 @@
|
||||
# Developer Guide
|
||||
|
||||
Guide for developers contributing to Amnezia VPN Web Panel.
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Local Development (without Docker)
|
||||
|
||||
1. **Install PHP 8.2+**
|
||||
```bash
|
||||
# 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
|
||||
```
|
||||
|
||||
2. **Install MySQL 8.0**
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt install mysql-server-8.0
|
||||
|
||||
# macOS
|
||||
brew install mysql@8.0
|
||||
```
|
||||
|
||||
3. **Install Composer**
|
||||
```bash
|
||||
curl -sS https://getcomposer.org/installer | php
|
||||
sudo mv composer.phar /usr/local/bin/composer
|
||||
```
|
||||
|
||||
4. **Clone and Setup**
|
||||
```bash
|
||||
git clone <repo-url>
|
||||
cd amnezia-web-panel
|
||||
composer install
|
||||
```
|
||||
|
||||
5. **Configure Database**
|
||||
```bash
|
||||
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;
|
||||
```
|
||||
|
||||
6. **Update Database Config**
|
||||
|
||||
Edit `inc/DB.php`:
|
||||
```php
|
||||
private static $config = [
|
||||
'host' => 'localhost', // Change from 'db'
|
||||
'dbname' => 'amnezia_panel',
|
||||
'user' => 'amnezia',
|
||||
'password' => 'amnezia123',
|
||||
'charset' => 'utf8mb4',
|
||||
];
|
||||
```
|
||||
|
||||
7. **Run Development Server**
|
||||
```bash
|
||||
cd public
|
||||
php -S localhost:8000
|
||||
```
|
||||
|
||||
Access: `http://localhost:8000`
|
||||
|
||||
### Docker Development (Recommended)
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```php
|
||||
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:
|
||||
|
||||
```php
|
||||
$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:
|
||||
|
||||
```php
|
||||
// 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:
|
||||
|
||||
```php
|
||||
View::render('template.twig', [
|
||||
'var1' => 'value1',
|
||||
'var2' => 'value2',
|
||||
]);
|
||||
```
|
||||
|
||||
#### 5. Models
|
||||
|
||||
**VpnServer** (`inc/VpnServer.php`):
|
||||
```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`):
|
||||
```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:
|
||||
|
||||
```php
|
||||
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`:
|
||||
```sql
|
||||
ALTER TABLE vpn_servers ADD COLUMN stats_json TEXT;
|
||||
```
|
||||
|
||||
**2. Add method to model**
|
||||
|
||||
Edit `inc/VpnServer.php`:
|
||||
```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`:
|
||||
```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`:
|
||||
```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`:
|
||||
```twig
|
||||
<a href="/servers/{{ server.id }}/stats">Statistics</a>
|
||||
```
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
### PHP
|
||||
|
||||
Follow PSR-12 coding standard:
|
||||
|
||||
```php
|
||||
<?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
|
||||
|
||||
```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
|
||||
|
||||
```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
|
||||
|
||||
```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:**
|
||||
```php
|
||||
$sql = "SELECT * FROM users WHERE email = '$email'";
|
||||
```
|
||||
|
||||
✅ **Always use prepared statements:**
|
||||
```php
|
||||
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?');
|
||||
$stmt->execute([$email]);
|
||||
```
|
||||
|
||||
### 2. XSS Prevention
|
||||
|
||||
❌ **Never output unescaped:**
|
||||
```php
|
||||
echo $_GET['name']; // Dangerous!
|
||||
```
|
||||
|
||||
✅ **Escape output:**
|
||||
```php
|
||||
echo htmlspecialchars($_GET['name'], ENT_QUOTES, 'UTF-8');
|
||||
```
|
||||
|
||||
In Twig (auto-escapes by default):
|
||||
```twig
|
||||
{{ user_input }} {# Safe #}
|
||||
{{ user_input|raw }} {# Unsafe - use carefully #}
|
||||
```
|
||||
|
||||
### 3. CSRF Protection
|
||||
|
||||
TODO: Implement token-based CSRF protection:
|
||||
|
||||
```php
|
||||
// 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:**
|
||||
```php
|
||||
// Hash
|
||||
$hash = password_hash($password, PASSWORD_BCRYPT);
|
||||
|
||||
// Verify
|
||||
if (password_verify($password, $hash)) {
|
||||
// Correct
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Input Validation
|
||||
|
||||
```php
|
||||
// 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)
|
||||
|
||||
```php
|
||||
// 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:
|
||||
```bash
|
||||
composer require --dev phpunit/phpunit
|
||||
./vendor/bin/phpunit tests/
|
||||
```
|
||||
|
||||
### Manual Testing
|
||||
|
||||
See [TESTING.md](TESTING.md) for comprehensive testing guide.
|
||||
|
||||
## Debugging
|
||||
|
||||
### Enable Error Display
|
||||
|
||||
In development, edit `public/index.php`:
|
||||
|
||||
```php
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
```
|
||||
|
||||
### Database Queries
|
||||
|
||||
```php
|
||||
// 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
|
||||
|
||||
```php
|
||||
// Add debug output
|
||||
$cmd = "your command";
|
||||
error_log("Executing SSH command: $cmd");
|
||||
$output = shell_exec($sshCmd);
|
||||
error_log("SSH output: $output");
|
||||
```
|
||||
|
||||
### Docker Logs
|
||||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
```php
|
||||
// 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)
|
||||
|
||||
```php
|
||||
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`:
|
||||
```env
|
||||
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:
|
||||
```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
|
||||
|
||||
- [PHP Documentation](https://www.php.net/docs.php)
|
||||
- [MySQL Reference](https://dev.mysql.com/doc/)
|
||||
- [Twig Documentation](https://twig.symfony.com/doc/)
|
||||
- [Tailwind CSS](https://tailwindcss.com/docs)
|
||||
- [Docker Documentation](https://docs.docker.com/)
|
||||
- [WireGuard Protocol](https://www.wireguard.com/)
|
||||
- [Amnezia VPN GitHub](https://github.com/amnezia-vpn/amnezia-client)
|
||||
|
||||
---
|
||||
|
||||
Happy coding! 🚀
|
||||
Reference in New Issue
Block a user