* @copyright Copyright (c) 2024 Kevin Feiler / AVVGO * @license MIT License * @link https://avvgo.de * @version 1.0.0 * * Dieses Modul ermöglicht die automatische Verwaltung von KeyHelp-Hosting-Accounts * direkt aus WHMCS heraus. Es unterstützt Erstellung, Sperrung, Entsperrung und * Löschung von Accounts sowie die Anzeige von Account-Informationen im Client-Bereich. * * Entwickelt von Kevin Feiler / AVVGO * * Kompatibel mit: * - PHP 8.3+ * - WHMCS 8.13+ * - KeyHelp (neueste stabile Version) */ if (!defined("WHMCS")) { die("This file cannot be accessed directly"); } use WHMCS\Database\Capsule; /** * Modul-Metadaten * * Definiert die grundlegenden Informationen über das Modul. * * @return array Modul-Metadaten */ function keyhelpmanager_MetaData() { return [ "DisplayName" => "KeyHelp Manager", "APIVersion" => "1.1", "RequiresServer" => true, "DefaultNonSSLPort" => "80", "DefaultSSLPort" => "443", "ServiceSingleSignOnLabel" => "Login zu KeyHelp", "AdminSingleSignOnLabel" => "Als Admin einloggen", ]; } /** * Modul-Konfigurationsoptionen * * Definiert die Felder, die in der Server-Konfiguration in WHMCS angezeigt werden. * * @return array Konfigurations-Array */ function keyhelpmanager_ConfigOptions() { return [ "hostname" => [ "FriendlyName" => "Hostname (IP or FQDN)", "Type" => "text", "Size" => "25", "Default" => "", "Description" => "Hostname oder IP-Adresse deines KeyHelp-Servers (ohne http:// oder https://)", "Required" => true, ], "apikey" => [ "FriendlyName" => "API Key", "Type" => "password", "Size" => "50", "Default" => "", "Description" => "Dein KeyHelp API-Schlüssel (zu finden in KeyHelp unter Einstellungen → API)", "Required" => true, ], "usessl" => [ "FriendlyName" => "Use SSL", "Type" => "yesno", "Default" => "on", "Description" => "SSL für die Verbindung zur API verwenden (dringend empfohlen)", ], "verify_ssl" => [ "FriendlyName" => "Verify SSL Certificate", "Type" => "yesno", "Default" => "on", "Description" => "SSL-Zertifikat verifizieren (bei selbstsignierten Zertifikaten deaktivieren)", ], ]; } /** * API-Request Hilfsfunktion * * Sendet HTTP-Anfragen an die KeyHelp-API und verarbeitet die Antworten. * * @param array $params WHMCS-Parameter-Array * @param string $endpoint API-Endpunkt (z.B. '/users', '/users/123') * @param string $method HTTP-Methode (GET, POST, PUT, DELETE) * @param array $data Request-Body-Daten * @return array Ergebnis mit 'success' (bool) und 'data' oder 'error' */ function _keyhelpmanager_APIRequest( array $params, string $endpoint, string $method = "GET", array $data = [], ) { try { // Konfiguration auslesen $hostname = $params["serverhostname"] ?? ($params["configoption1"] ?? ""); $apiKey = $params["serverpassword"] ?? ($params["configoption2"] ?? ""); $useSSL = $params["configoption3"] ?? "on"; $verifySSL = $params["configoption4"] ?? "on"; // Validierung if (empty($hostname)) { return [ "success" => false, "error" => "KeyHelp-Hostname ist nicht konfiguriert.", ]; } if (empty($apiKey)) { return [ "success" => false, "error" => "KeyHelp API-Schlüssel ist nicht konfiguriert.", ]; } // Protocol bestimmen $protocol = $useSSL === "on" ? "https" : "http"; // Basis-URL zusammenbauen $baseUrl = sprintf("%s://%s/api/v2", $protocol, $hostname); $url = $baseUrl . $endpoint; // Guzzle HTTP-Client initialisieren $client = new \GuzzleHttp\Client([ "verify" => $verifySSL === "on", "timeout" => 30, "http_errors" => false, ]); // Request-Optionen $options = [ "headers" => [ "X-API-Key" => $apiKey, "Content-Type" => "application/json", "Accept" => "application/json", ], ]; // Body-Daten hinzufügen wenn vorhanden if (!empty($data) && in_array($method, ["POST", "PUT", "PATCH"])) { $options["json"] = $data; } // API-Request ausführen $response = $client->request($method, $url, $options); $statusCode = $response->getStatusCode(); $body = (string) $response->getBody(); // JSON-Antwort dekodieren $responseData = json_decode($body, true); // Logging für Debugging logModuleCall( "keyhelpmanager", $method . " " . $endpoint, $data, $statusCode . " - " . $body, $responseData, [$apiKey], // API-Key in Logs maskieren ); // Erfolgreiche Antworten (2xx) if ($statusCode >= 200 && $statusCode < 300) { return [ "success" => true, "data" => $responseData, "status_code" => $statusCode, ]; } // Fehlerbehandlung $errorMessage = "KeyHelp API-Fehler: "; if (isset($responseData["message"])) { $errorMessage .= $responseData["message"]; } elseif (isset($responseData["error"])) { $errorMessage .= $responseData["error"]; } else { $errorMessage .= sprintf("HTTP Status %d - %s", $statusCode, $body); } return [ "success" => false, "error" => $errorMessage, "status_code" => $statusCode, ]; } catch (\GuzzleHttp\Exception\ConnectException $e) { return [ "success" => false, "error" => "Verbindung zum KeyHelp-Server fehlgeschlagen: " . $e->getMessage(), ]; } catch (\GuzzleHttp\Exception\RequestException $e) { return [ "success" => false, "error" => "API-Request fehlgeschlagen: " . $e->getMessage(), ]; } catch (\Exception $e) { return [ "success" => false, "error" => "Unerwarteter Fehler: " . $e->getMessage(), ]; } } /** * Account erstellen * * Wird von WHMCS nach erfolgreicher Bezahlung aufgerufen. * Erstellt einen neuen Benutzer-Account in KeyHelp. * * @param array $params WHMCS-Parameter * @return string Erfolgs- oder Fehlermeldung */ function keyhelpmanager_CreateAccount(array $params) { try { // Extrahiere notwendige Parameter $domain = $params["domain"] ?? ""; $username = $params["username"] ?? ""; $password = $params["password"] ?? ""; $clientEmail = $params["clientsdetails"]["email"] ?? ""; $firstName = $params["clientsdetails"]["firstname"] ?? ""; $lastName = $params["clientsdetails"]["lastname"] ?? ""; // Validierung if (empty($domain)) { return "Domain ist erforderlich."; } if (empty($username)) { // Generiere Benutzernamen aus Domain wenn nicht vorhanden $username = _keyhelpmanager_GenerateUsername($domain); } if (empty($password)) { // Generiere sicheres Passwort wenn nicht vorhanden $password = _keyhelpmanager_GeneratePassword(); } // Account-Daten für KeyHelp API vorbereiten $accountData = [ "login_name" => $username, "password" => $password, "email" => $clientEmail, "display_name" => trim($firstName . " " . $lastName), ]; // Optional: Plan/Tarif aus konfigurierbaren Optionen if (!empty($params["configoptions"]["KeyHelp Plan"])) { $accountData["plan"] = $params["configoptions"]["KeyHelp Plan"]; } // Benutzer über API erstellen $result = _keyhelpmanager_APIRequest( $params, "/users", "POST", $accountData, ); if (!$result["success"]) { return $result["error"]; } $userId = $result["data"]["id"] ?? null; if (empty($userId)) { return "Benutzer wurde erstellt, aber keine User-ID zurückgegeben."; } // Domain zum Benutzer hinzufügen $domainData = [ "domain_name" => $domain, "user_id" => $userId, ]; $domainResult = _keyhelpmanager_APIRequest( $params, "/domains", "POST", $domainData, ); if (!$domainResult["success"]) { // Rollback: Benutzer wieder löschen _keyhelpmanager_APIRequest($params, "/users/" . $userId, "DELETE"); return "Domain konnte nicht hinzugefügt werden: " . $domainResult["error"]; } // Speichere Username und Passwort in WHMCS Custom Fields _keyhelpmanager_SaveAccountDetails($params["serviceid"], [ "username" => $username, "password" => $password, "userid" => $userId, ]); return "success"; } catch (\Exception $e) { return "Fehler beim Erstellen des Accounts: " . $e->getMessage(); } } /** * Account sperren * * Wird von WHMCS aufgerufen, wenn ein Account gesperrt werden soll * (z.B. bei Zahlungsverzug). * * @param array $params WHMCS-Parameter * @return string Erfolgs- oder Fehlermeldung */ function keyhelpmanager_SuspendAccount(array $params) { try { $userId = _keyhelpmanager_GetUserId($params); if (empty($userId)) { return "Benutzer-ID nicht gefunden. Account wurde möglicherweise nicht korrekt erstellt."; } // Benutzer-Status auf "gesperrt" setzen $updateData = [ "is_locked" => true, ]; $result = _keyhelpmanager_APIRequest( $params, "/users/" . $userId, "PUT", $updateData, ); if (!$result["success"]) { return $result["error"]; } return "success"; } catch (\Exception $e) { return "Fehler beim Sperren des Accounts: " . $e->getMessage(); } } /** * Account entsperren * * Wird von WHMCS aufgerufen, wenn ein gesperrter Account wieder * aktiviert werden soll. * * @param array $params WHMCS-Parameter * @return string Erfolgs- oder Fehlermeldung */ function keyhelpmanager_UnsuspendAccount(array $params) { try { $userId = _keyhelpmanager_GetUserId($params); if (empty($userId)) { return "Benutzer-ID nicht gefunden. Account wurde möglicherweise nicht korrekt erstellt."; } // Benutzer-Status auf "aktiv" setzen $updateData = [ "is_locked" => false, ]; $result = _keyhelpmanager_APIRequest( $params, "/users/" . $userId, "PUT", $updateData, ); if (!$result["success"]) { return $result["error"]; } return "success"; } catch (\Exception $e) { return "Fehler beim Entsperren des Accounts: " . $e->getMessage(); } } /** * Account löschen * * Wird von WHMCS aufgerufen, wenn ein Account endgültig gelöscht werden soll. * ACHTUNG: Dies löscht alle Daten unwiderruflich! * * @param array $params WHMCS-Parameter * @return string Erfolgs- oder Fehlermeldung */ function keyhelpmanager_TerminateAccount(array $params) { try { $userId = _keyhelpmanager_GetUserId($params); if (empty($userId)) { return "Benutzer-ID nicht gefunden. Account wurde möglicherweise bereits gelöscht."; } // Benutzer und alle zugehörigen Daten löschen $result = _keyhelpmanager_APIRequest( $params, "/users/" . $userId, "DELETE", ); if (!$result["success"]) { return $result["error"]; } // Entferne gespeicherte Account-Details _keyhelpmanager_DeleteAccountDetails($params["serviceid"]); return "success"; } catch (\Exception $e) { return "Fehler beim Löschen des Accounts: " . $e->getMessage(); } } /** * Client-Bereich * * Gibt Informationen für den Client-Bereich in WHMCS zurück. * Zeigt Login-Details und Hosting-Statistiken an. * * @param array $params WHMCS-Parameter * @return array Template-Variablen */ function keyhelpmanager_ClientArea(array $params) { try { $accountDetails = _keyhelpmanager_GetAccountDetails( $params["serviceid"], ); $userId = $accountDetails["userid"] ?? null; $hostname = $params["serverhostname"] ?? ($params["configoption1"] ?? ""); $useSSL = $params["configoption3"] ?? "on"; $protocol = $useSSL === "on" ? "https" : "http"; // Login-URL $loginUrl = sprintf("%s://%s", $protocol, $hostname); // Basis-Informationen $templateVars = [ "login_url" => $loginUrl, "username" => $accountDetails["username"] ?? ($params["username"] ?? "Nicht verfügbar"), "password" => $accountDetails["password"] ?? ($params["password"] ?? "••••••••"), "domain" => $params["domain"] ?? "Nicht verfügbar", "stats" => [], "error" => null, ]; // Statistiken von KeyHelp API abrufen wenn User-ID vorhanden if (!empty($userId)) { $statsResult = _keyhelpmanager_APIRequest( $params, "/users/" . $userId . "/statistics", "GET", ); if ($statsResult["success"] && isset($statsResult["data"])) { $stats = $statsResult["data"]; $templateVars["stats"] = [ "disk_used" => _keyhelpmanager_FormatBytes( $stats["disk_usage"] ?? 0, ), "disk_limit" => _keyhelpmanager_FormatBytes( $stats["disk_limit"] ?? 0, ), "disk_percent" => _keyhelpmanager_CalculatePercent( $stats["disk_usage"] ?? 0, $stats["disk_limit"] ?? 0, ), "bandwidth_used" => _keyhelpmanager_FormatBytes( $stats["traffic_usage"] ?? 0, ), "bandwidth_limit" => _keyhelpmanager_FormatBytes( $stats["traffic_limit"] ?? 0, ), "bandwidth_percent" => _keyhelpmanager_CalculatePercent( $stats["traffic_usage"] ?? 0, $stats["traffic_limit"] ?? 0, ), "domains" => $stats["domains_count"] ?? 0, "databases" => $stats["databases_count"] ?? 0, "email_accounts" => $stats["email_accounts_count"] ?? 0, ]; } } return [ "templatefile" => "clientarea", "vars" => $templateVars, ]; } catch (\Exception $e) { return [ "templatefile" => "clientarea", "vars" => [ "error" => "Fehler beim Laden der Account-Informationen: " . $e->getMessage(), ], ]; } } /** * Admin-Login-Link (Single Sign-On) * * Erstellt einen direkten Login-Link für Admins, um sich im KeyHelp-Panel * des Kunden anzumelden, ohne das Passwort eingeben zu müssen. * * @param array $params WHMCS-Parameter * @return array Login-Link-Konfiguration */ function keyhelpmanager_LoginLink(array $params) { try { $accountDetails = _keyhelpmanager_GetAccountDetails( $params["serviceid"], ); $userId = $accountDetails["userid"] ?? null; if (empty($userId)) { return [ "success" => false, "errorMsg" => "Benutzer-ID nicht gefunden.", ]; } // Session-Token von KeyHelp API anfordern $sessionData = [ "user_id" => $userId, "lifetime" => 300, // 5 Minuten Gültigkeit ]; $result = _keyhelpmanager_APIRequest( $params, "/sessions", "POST", $sessionData, ); if (!$result["success"]) { return [ "success" => false, "errorMsg" => "Login-Session konnte nicht erstellt werden: " . $result["error"], ]; } $sessionToken = $result["data"]["token"] ?? null; if (empty($sessionToken)) { return [ "success" => false, "errorMsg" => "Kein Session-Token erhalten.", ]; } // Login-URL mit Session-Token $hostname = $params["serverhostname"] ?? ($params["configoption1"] ?? ""); $useSSL = $params["configoption3"] ?? "on"; $protocol = $useSSL === "on" ? "https" : "http"; $loginUrl = sprintf( "%s://%s/login?token=%s", $protocol, $hostname, $sessionToken, ); return [ "success" => true, "redirectTo" => $loginUrl, ]; } catch (\Exception $e) { return [ "success" => false, "errorMsg" => "Fehler beim Erstellen des Login-Links: " . $e->getMessage(), ]; } } /** * Passwort ändern * * Ermöglicht das Ändern des Passworts für einen KeyHelp-Account. * * @param array $params WHMCS-Parameter * @return string Erfolgs- oder Fehlermeldung */ function keyhelpmanager_ChangePassword(array $params) { try { $userId = _keyhelpmanager_GetUserId($params); $newPassword = $params["password"] ?? ""; if (empty($userId)) { return "Benutzer-ID nicht gefunden."; } if (empty($newPassword)) { return "Neues Passwort ist erforderlich."; } // Passwort über API ändern $updateData = [ "password" => $newPassword, ]; $result = _keyhelpmanager_APIRequest( $params, "/users/" . $userId, "PUT", $updateData, ); if (!$result["success"]) { return $result["error"]; } // Aktualisiere gespeichertes Passwort _keyhelpmanager_UpdateAccountDetail( $params["serviceid"], "password", $newPassword, ); return "success"; } catch (\Exception $e) { return "Fehler beim Ändern des Passworts: " . $e->getMessage(); } } /** * Test-Verbindung * * Testet die Verbindung zur KeyHelp-API. * * @param array $params WHMCS-Parameter * @return array Test-Ergebnis */ function keyhelpmanager_TestConnection(array $params) { try { // Einfache API-Abfrage zum Testen $result = _keyhelpmanager_APIRequest($params, "/server/version", "GET"); if (!$result["success"]) { return [ "success" => false, "error" => $result["error"], ]; } $version = $result["data"]["version"] ?? "Unbekannt"; return [ "success" => true, "message" => sprintf( "Verbindung erfolgreich! KeyHelp Version: %s", $version, ), ]; } catch (\Exception $e) { return [ "success" => false, "error" => $e->getMessage(), ]; } } // ============================================================================ // HILFSFUNKTIONEN // ============================================================================ /** * Generiert einen sicheren Benutzernamen aus einer Domain * * @param string $domain Domain-Name * @return string Generierter Benutzername */ function _keyhelpmanager_GenerateUsername(string $domain): string { // Entferne TLD und Sonderzeichen $username = preg_replace('/\.[^.]+$/', "", $domain); $username = preg_replace("/[^a-z0-9]/i", "", $username); $username = strtolower($username); // Begrenze auf 16 Zeichen $username = substr($username, 0, 16); // Füge zufällige Zahlen hinzu wenn zu kurz if (strlen($username) < 6) { $username .= rand(1000, 9999); } return $username; } /** * Generiert ein sicheres Zufallspasswort * * @param int $length Passwort-Länge * @return string Generiertes Passwort */ function _keyhelpmanager_GeneratePassword(int $length = 16): string { $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+'; $password = ""; $charsLength = strlen($chars); for ($i = 0; $i < $length; $i++) { $password .= $chars[random_int(0, $charsLength - 1)]; } return $password; } /** * Speichert Account-Details in der WHMCS-Datenbank * * @param int $serviceId Service-ID * @param array $details Account-Details (username, password, userid) * @return void */ function _keyhelpmanager_SaveAccountDetails( int $serviceId, array $details, ): void { try { foreach ($details as $key => $value) { Capsule::table("tblhosting") ->where("id", $serviceId) ->update([ $key === "username" ? "username" : "password" => $value, ]); } // Speichere userid in Custom Field oder Service Properties if (isset($details["userid"])) { Capsule::table("tblcustomfieldsvalues")->updateOrInsert( [ "relid" => $serviceId, "fieldid" => _keyhelpmanager_GetCustomFieldId( "KeyHelp User ID", ), ], [ "value" => $details["userid"], ], ); } } catch (\Exception $e) { logActivity( "KeyHelpManager: Fehler beim Speichern der Account-Details: " . $e->getMessage(), ); } } /** * Lädt Account-Details aus der WHMCS-Datenbank * * @param int $serviceId Service-ID * @return array Account-Details */ function _keyhelpmanager_GetAccountDetails(int $serviceId): array { try { $service = Capsule::table("tblhosting") ->where("id", $serviceId) ->first(); $userid = Capsule::table("tblcustomfieldsvalues") ->where("relid", $serviceId) ->where( "fieldid", _keyhelpmanager_GetCustomFieldId("KeyHelp User ID"), ) ->value("value"); return [ "username" => $service->username ?? "", "password" => decrypt($service->password ?? ""), "userid" => $userid ?? null, ]; } catch (\Exception $e) { logActivity( "KeyHelpManager: Fehler beim Laden der Account-Details: " . $e->getMessage(), ); return []; } } /** * Holt die KeyHelp User-ID für einen Service * * @param array $params WHMCS-Parameter * @return int|null User-ID oder null */ function _keyhelpmanager_GetUserId(array $params): ?int { $accountDetails = _keyhelpmanager_GetAccountDetails($params["serviceid"]); return $accountDetails["userid"] ?? null; } /** * Aktualisiert ein einzelnes Account-Detail * * @param int $serviceId Service-ID * @param string $field Feldname * @param string $value Wert * @return void */ function _keyhelpmanager_UpdateAccountDetail( int $serviceId, string $field, string $value, ): void { try { if ($field === "username" || $field === "password") { Capsule::table("tblhosting") ->where("id", $serviceId) ->update([ $field => $value, ]); } } catch (\Exception $e) { logActivity( "KeyHelpManager: Fehler beim Aktualisieren von " . $field . ": " . $e->getMessage(), ); } } /** * Löscht gespeicherte Account-Details * * @param int $serviceId Service-ID * @return void */ function _keyhelpmanager_DeleteAccountDetails(int $serviceId): void { try { Capsule::table("tblcustomfieldsvalues") ->where("relid", $serviceId) ->delete(); } catch (\Exception $e) { logActivity( "KeyHelpManager: Fehler beim Löschen der Account-Details: " . $e->getMessage(), ); } } /** * Holt oder erstellt die Custom Field ID für ein Feld * * @param string $fieldName Feldname * @return int Field-ID */ function _keyhelpmanager_GetCustomFieldId(string $fieldName): int { try { $field = Capsule::table("tblcustomfields") ->where("fieldname", $fieldName) ->where("type", "product") ->first(); if (!$field) { // Erstelle Custom Field wenn nicht vorhanden $fieldId = Capsule::table("tblcustomfields")->insertGetId([ "type" => "product", "fieldname" => $fieldName, "fieldtype" => "text", "description" => "", "adminonly" => "on", ]); return $fieldId; } return $field->id; } catch (\Exception $e) { logActivity( "KeyHelpManager: Fehler beim Abrufen der Custom Field ID: " . $e->getMessage(), ); return 0; } } /** * Formatiert Bytes in eine lesbare Größe * * @param int $bytes Bytes * @return string Formatierte Größe */ function _keyhelpmanager_FormatBytes(int $bytes): string { $units = ["B", "KB", "MB", "GB", "TB"]; $power = $bytes > 0 ? floor(log($bytes, 1024)) : 0; return number_format($bytes / pow(1024, $power), 2, ",", ".") . " " . $units[$power]; } /** * Berechnet Prozentsatz * * @param int $used Verbrauchter Wert * @param int $limit Limit * @return float Prozentsatz */ function _keyhelpmanager_CalculatePercent(int $used, int $limit): float { if ($limit == 0) { return 0; } return round(($used / $limit) * 100, 2); }