feat: ssh auth, protocol management, and cleanup
This commit is contained in:
@@ -0,0 +1,407 @@
|
||||
<?php
|
||||
|
||||
class ProtocolService
|
||||
{
|
||||
|
||||
/**
|
||||
* Get all protocols with additional metadata
|
||||
*/
|
||||
public static function getAllProtocolsWithStats(): array
|
||||
{
|
||||
try {
|
||||
$pdo = DB::conn();
|
||||
$stmt = $pdo->query('
|
||||
SELECT p.*,
|
||||
COUNT(DISTINCT sp.server_id) as server_count,
|
||||
COUNT(DISTINCT pt.id) as template_count,
|
||||
COUNT(DISTINCT pv.id) as variable_count,
|
||||
COUNT(DISTINCT ag.id) as ai_generation_count,
|
||||
MAX(ag.created_at) as last_ai_generation
|
||||
FROM protocols p
|
||||
LEFT JOIN server_protocols sp ON p.id = sp.protocol_id
|
||||
LEFT JOIN protocol_templates pt ON p.id = pt.protocol_id
|
||||
LEFT JOIN protocol_variables pv ON p.id = pv.protocol_id
|
||||
LEFT JOIN ai_generations ag ON p.id = ag.protocol_id
|
||||
GROUP BY p.id
|
||||
ORDER BY p.name ASC
|
||||
');
|
||||
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Error in ProtocolService::getAllProtocolsWithStats: " . $e->getMessage());
|
||||
throw new Exception('Failed to get protocols with stats');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get protocol with all related data (templates, variables, AI history)
|
||||
*/
|
||||
public static function getProtocolWithDetails(int $protocolId): array
|
||||
{
|
||||
try {
|
||||
$pdo = DB::conn();
|
||||
|
||||
// Get protocol
|
||||
$stmt = $pdo->prepare('SELECT * FROM protocols WHERE id = ?');
|
||||
$stmt->execute([$protocolId]);
|
||||
$protocol = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$protocol) {
|
||||
throw new Exception('Protocol not found');
|
||||
}
|
||||
|
||||
// Get templates
|
||||
$stmt = $pdo->prepare('SELECT * FROM protocol_templates WHERE protocol_id = ? ORDER BY is_default DESC, template_name ASC');
|
||||
$stmt->execute([$protocolId]);
|
||||
$protocol['templates'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Get variables
|
||||
$stmt = $pdo->prepare('SELECT * FROM protocol_variables WHERE protocol_id = ? ORDER BY variable_name ASC');
|
||||
$stmt->execute([$protocolId]);
|
||||
$protocol['variables'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Get AI generation history (last 10)
|
||||
$stmt = $pdo->prepare('
|
||||
SELECT ag.*, p.name as protocol_name
|
||||
FROM ai_generations ag
|
||||
LEFT JOIN protocols p ON ag.protocol_id = p.id
|
||||
WHERE ag.protocol_id = ?
|
||||
ORDER BY ag.created_at DESC
|
||||
LIMIT 10
|
||||
');
|
||||
$stmt->execute([$protocolId]);
|
||||
$protocol['ai_history'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Get server usage
|
||||
$stmt = $pdo->prepare('
|
||||
SELECT sp.*, vs.name as server_name, vs.host as server_host
|
||||
FROM server_protocols sp
|
||||
JOIN vpn_servers vs ON sp.server_id = vs.id
|
||||
WHERE sp.protocol_id = ?
|
||||
ORDER BY sp.applied_at DESC
|
||||
');
|
||||
$stmt->execute([$protocolId]);
|
||||
$protocol['server_usage'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
return $protocol;
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Error in ProtocolService::getProtocolWithDetails: " . $e->getMessage());
|
||||
throw new Exception('Failed to get protocol details');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate protocol data before saving
|
||||
*/
|
||||
public static function validateProtocolData(array $data): array
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
// Validate name
|
||||
if (empty($data['name'])) {
|
||||
$errors[] = 'Protocol name is required';
|
||||
} elseif (strlen($data['name']) > 255) {
|
||||
$errors[] = 'Protocol name must be less than 255 characters';
|
||||
}
|
||||
|
||||
// Validate slug
|
||||
if (empty($data['slug'])) {
|
||||
$errors[] = 'Protocol slug is required';
|
||||
} elseif (!preg_match('/^[a-z0-9_-]+$/i', $data['slug'])) {
|
||||
$errors[] = 'Slug may contain only letters, numbers, dashes, and underscores';
|
||||
} elseif (strlen($data['slug']) > 100) {
|
||||
$errors[] = 'Protocol slug must be less than 100 characters';
|
||||
}
|
||||
|
||||
// Validate description length
|
||||
if (isset($data['description']) && strlen($data['description']) > 65535) {
|
||||
$errors[] = 'Description is too long';
|
||||
}
|
||||
|
||||
// Validate install script
|
||||
if (isset($data['install_script']) && strlen($data['install_script']) > 16777215) { // MEDIUMTEXT limit
|
||||
$errors[] = 'Installation script is too long';
|
||||
}
|
||||
|
||||
// Validate output template
|
||||
if (isset($data['output_template']) && strlen($data['output_template']) > 16777215) { // MEDIUMTEXT limit
|
||||
$errors[] = 'Output template is too long';
|
||||
}
|
||||
|
||||
// Validate ubuntu_compatible
|
||||
if (isset($data['ubuntu_compatible']) && !is_bool($data['ubuntu_compatible']) && !in_array($data['ubuntu_compatible'], [0, 1, '0', '1'])) {
|
||||
$errors[] = 'Ubuntu compatible must be a boolean value';
|
||||
}
|
||||
|
||||
// Validate is_active
|
||||
if (isset($data['is_active']) && !is_bool($data['is_active']) && !in_array($data['is_active'], [0, 1, '0', '1'])) {
|
||||
$errors[] = 'Active status must be a boolean value';
|
||||
}
|
||||
|
||||
// Validate QR code template
|
||||
if (isset($data['qr_code_template']) && strlen($data['qr_code_template']) > 16777215) {
|
||||
$errors[] = 'QR code template is too long';
|
||||
}
|
||||
|
||||
// Validate QR code format
|
||||
if (isset($data['qr_code_format']) && !in_array($data['qr_code_format'], ['raw', 'amnezia_compressed'])) {
|
||||
$errors[] = 'Invalid QR code format';
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if slug is unique
|
||||
*/
|
||||
public static function isSlugUnique(string $slug, ?int $excludeId = null): bool
|
||||
{
|
||||
try {
|
||||
$pdo = DB::conn();
|
||||
|
||||
if ($excludeId) {
|
||||
$stmt = $pdo->prepare('SELECT COUNT(*) FROM protocols WHERE slug = ? AND id != ?');
|
||||
$stmt->execute([$slug, $excludeId]);
|
||||
} else {
|
||||
$stmt = $pdo->prepare('SELECT COUNT(*) FROM protocols WHERE slug = ?');
|
||||
$stmt->execute([$slug]);
|
||||
}
|
||||
|
||||
return (int) $stmt->fetchColumn() === 0;
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Error in ProtocolService::isSlugUnique: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if protocol can be deleted
|
||||
*/
|
||||
public static function canDeleteProtocol(int $protocolId): array
|
||||
{
|
||||
try {
|
||||
$pdo = DB::conn();
|
||||
|
||||
// Check if protocol is used by any servers
|
||||
$stmt = $pdo->prepare('SELECT COUNT(*) FROM server_protocols WHERE protocol_id = ?');
|
||||
$stmt->execute([$protocolId]);
|
||||
$serverCount = (int) $stmt->fetchColumn();
|
||||
|
||||
$canDelete = $serverCount === 0;
|
||||
$reason = '';
|
||||
|
||||
if (!$canDelete) {
|
||||
$reason = "Protocol is currently used by $serverCount server(s)";
|
||||
}
|
||||
|
||||
return [
|
||||
'can_delete' => $canDelete,
|
||||
'reason' => $reason,
|
||||
'server_count' => $serverCount
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Error in ProtocolService::canDeleteProtocol: " . $e->getMessage());
|
||||
return [
|
||||
'can_delete' => false,
|
||||
'reason' => 'Database error occurred',
|
||||
'server_count' => 0
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate protocol template with variables
|
||||
*/
|
||||
public static function generateProtocolOutput(array $protocol, array $variables): string
|
||||
{
|
||||
try {
|
||||
$template = $protocol['output_template'] ?? '';
|
||||
|
||||
if (empty($template)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
foreach ($variables as $key => $value) {
|
||||
$template = str_replace('{{' . $key . '}}', $value ?? '', $template);
|
||||
}
|
||||
$template = preg_replace('/(\w+:\/\/[^\/:]+):(?=\/|\?|$)/', '$1', $template);
|
||||
$template = preg_replace('/(@[^\/:]+):(?=\/|\?|$)/', '$1', $template);
|
||||
$template = preg_replace('/(\w+:\/\/)@(?=[^\/]{1})/', '$1', $template);
|
||||
$template = preg_replace('/\{\{[^}]+\}\}/', '', $template);
|
||||
|
||||
// Check for unreplaced variables
|
||||
if (preg_match('/\{\{([^}]+)\}\}/', $template, $matches)) {
|
||||
error_log("Unreplaced variables in protocol template: " . implode(', ', $matches));
|
||||
}
|
||||
|
||||
return $template;
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Error in ProtocolService::generateProtocolOutput: " . $e->getMessage());
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate QR code payload from template
|
||||
*/
|
||||
public static function generateQrCodePayload(array $protocol, array $variables): string
|
||||
{
|
||||
try {
|
||||
$template = $protocol['qr_code_template'] ?? '';
|
||||
$format = $protocol['qr_code_format'] ?? 'amnezia_compressed';
|
||||
|
||||
if (empty($template)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Render template using the same logic as output template
|
||||
// We temporarily wrap it to use the existing method
|
||||
$rendered = self::generateProtocolOutput(['output_template' => $template], $variables);
|
||||
|
||||
if ($format === 'amnezia_compressed') {
|
||||
require_once __DIR__ . '/QrUtil.php';
|
||||
return QrUtil::encodeOldPayloadFromJson($rendered);
|
||||
}
|
||||
|
||||
// For 'raw' and 'text' formats, return rendered template directly
|
||||
return $rendered;
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Error in ProtocolService::generateQrCodePayload: " . $e->getMessage());
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get protocol statistics for dashboard
|
||||
*/
|
||||
public static function getProtocolStatistics(): array
|
||||
{
|
||||
try {
|
||||
$pdo = DB::conn();
|
||||
|
||||
// Total protocols
|
||||
$stmt = $pdo->query('SELECT COUNT(*) FROM protocols');
|
||||
$totalProtocols = (int) $stmt->fetchColumn();
|
||||
|
||||
// Active protocols
|
||||
$stmt = $pdo->query('SELECT COUNT(*) FROM protocols WHERE is_active = 1');
|
||||
$activeProtocols = (int) $stmt->fetchColumn();
|
||||
|
||||
// Ubuntu compatible protocols
|
||||
$stmt = $pdo->query('SELECT COUNT(*) FROM protocols WHERE ubuntu_compatible = 1');
|
||||
$ubuntuCompatibleProtocols = (int) $stmt->fetchColumn();
|
||||
|
||||
// Protocols with AI generations
|
||||
$stmt = $pdo->query('
|
||||
SELECT COUNT(DISTINCT protocol_id)
|
||||
FROM ai_generations
|
||||
WHERE protocol_id IS NOT NULL
|
||||
');
|
||||
$protocolsWithAI = (int) $stmt->fetchColumn();
|
||||
|
||||
// Recent AI generations
|
||||
$stmt = $pdo->query('
|
||||
SELECT COUNT(*)
|
||||
FROM ai_generations
|
||||
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
|
||||
');
|
||||
$recentAIGenerations = (int) $stmt->fetchColumn();
|
||||
|
||||
// Server usage by protocol
|
||||
$stmt = $pdo->query('
|
||||
SELECT p.name, COUNT(sp.server_id) as server_count
|
||||
FROM protocols p
|
||||
LEFT JOIN server_protocols sp ON p.id = sp.protocol_id
|
||||
GROUP BY p.id, p.name
|
||||
ORDER BY server_count DESC
|
||||
LIMIT 10
|
||||
');
|
||||
$serverUsageByProtocol = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
return [
|
||||
'total_protocols' => $totalProtocols,
|
||||
'active_protocols' => $activeProtocols,
|
||||
'ubuntu_compatible_protocols' => $ubuntuCompatibleProtocols,
|
||||
'protocols_with_ai' => $protocolsWithAI,
|
||||
'recent_ai_generations' => $recentAIGenerations,
|
||||
'server_usage_by_protocol' => $serverUsageByProtocol,
|
||||
'ai_usage_percentage' => $totalProtocols > 0 ? round(($protocolsWithAI / $totalProtocols) * 100, 2) : 0
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Error in ProtocolService::getProtocolStatistics: " . $e->getMessage());
|
||||
return [
|
||||
'total_protocols' => 0,
|
||||
'active_protocols' => 0,
|
||||
'ubuntu_compatible_protocols' => 0,
|
||||
'protocols_with_ai' => 0,
|
||||
'recent_ai_generations' => 0,
|
||||
'server_usage_by_protocol' => [],
|
||||
'ai_usage_percentage' => 0
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get AI generation statistics
|
||||
*/
|
||||
public static function getAIGenerationStatistics(): array
|
||||
{
|
||||
try {
|
||||
$pdo = DB::conn();
|
||||
|
||||
// Total AI generations
|
||||
$stmt = $pdo->query('SELECT COUNT(*) FROM ai_generations');
|
||||
$totalGenerations = (int) $stmt->fetchColumn();
|
||||
|
||||
// AI generations this month
|
||||
$stmt = $pdo->query('
|
||||
SELECT COUNT(*)
|
||||
FROM ai_generations
|
||||
WHERE MONTH(created_at) = MONTH(NOW()) AND YEAR(created_at) = YEAR(NOW())
|
||||
');
|
||||
$thisMonthGenerations = (int) $stmt->fetchColumn();
|
||||
|
||||
// AI generations by model
|
||||
$stmt = $pdo->query('
|
||||
SELECT model_used, COUNT(*) as count
|
||||
FROM ai_generations
|
||||
GROUP BY model_used
|
||||
ORDER BY count DESC
|
||||
');
|
||||
$generationsByModel = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Ubuntu compatible generations
|
||||
$stmt = $pdo->query('
|
||||
SELECT COUNT(*)
|
||||
FROM ai_generations
|
||||
WHERE ubuntu_compatible = 1
|
||||
');
|
||||
$ubuntuCompatibleGenerations = (int) $stmt->fetchColumn();
|
||||
|
||||
return [
|
||||
'total_generations' => $totalGenerations,
|
||||
'this_month_generations' => $thisMonthGenerations,
|
||||
'generations_by_model' => $generationsByModel,
|
||||
'ubuntu_compatible_generations' => $ubuntuCompatibleGenerations,
|
||||
'ubuntu_compatible_percentage' => $totalGenerations > 0 ? round(($ubuntuCompatibleGenerations / $totalGenerations) * 100, 2) : 0
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Error in ProtocolService::getAIGenerationStatistics: " . $e->getMessage());
|
||||
return [
|
||||
'total_generations' => 0,
|
||||
'this_month_generations' => 0,
|
||||
'generations_by_model' => [],
|
||||
'ubuntu_compatible_generations' => 0,
|
||||
'ubuntu_compatible_percentage' => 0
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user