From 98893924ac99029b421b953310bbd6fc908d6419 Mon Sep 17 00:00:00 2001 From: Nebulae Date: Wed, 19 Aug 2026 18:31:02 +0200 Subject: [PATCH] feat(ISPConfig Webhosting): add creation script for members with website --- PROJECT_STRUCTURE.md | 3 +- app/Console/Commands/CreateISPWebAccounts.php | 314 +++++++++++++++++- .../Commands/SyncISPConfigWebMembers.php | 52 ++- .../ISPConfig/ISPConfigWebService.php | 263 ++++++++++++--- .../Commands/CreateISPWebAccountsTest.php | 68 ++++ 5 files changed, 596 insertions(+), 104 deletions(-) create mode 100644 tests/Feature/Commands/CreateISPWebAccountsTest.php diff --git a/PROJECT_STRUCTURE.md b/PROJECT_STRUCTURE.md index 28bc874..8026de8 100644 --- a/PROJECT_STRUCTURE.md +++ b/PROJECT_STRUCTURE.md @@ -88,7 +88,7 @@ Elle gère les membres, les cotisations, et s'intègre avec des services tiers ( | MemberService | Inscription et desactivation de membres | | DolibarrService | Integration ERP via API REST | | ISPConfigMailService | Gestion comptes mail via SOAP | -| ISPConfigWebService | Gestion hebergement web via SOAP (avec cache) | +| ISPConfigWebService | Gestion hebergement web via SOAP (avec cache). Creation de clients et reassignation de sites | | NextcloudService | Gestion comptes Nextcloud via OCS (avec cache 7 jours) | --- @@ -101,6 +101,7 @@ Elle gère les membres, les cotisations, et s'intègre avec des services tiers ( | `members:cleanup-expired` | Desactive les membres expires (Dolibarr + ISPConfig + Nextcloud). `--dry-run` | | `sync:ispconfig-mail-members` | Lie les membres a leurs comptes mail ISPConfig (@retzien.fr) | | `sync:ispconfig-web-members` | Lie les membres a leurs comptes d'hebergement web | +| `ext:create-isp-accounts` | Cree les clients ISPConfig pour les membres et reassigne leurs sites. `--dry-run`, `--force` | | `nextcloud:sync-members` | Lie les membres a leurs comptes Nextcloud | | `memberships:sync-services` | Synchronise les services associes aux membres | diff --git a/app/Console/Commands/CreateISPWebAccounts.php b/app/Console/Commands/CreateISPWebAccounts.php index 3c2fb26..edcb1b6 100644 --- a/app/Console/Commands/CreateISPWebAccounts.php +++ b/app/Console/Commands/CreateISPWebAccounts.php @@ -2,29 +2,311 @@ namespace App\Console\Commands; +use App\Enums\IspconfigType; +use App\Models\IspconfigMember; +use App\Models\Member; +use App\Services\ISPConfig\ISPConfigWebService; +use Exception; use Illuminate\Console\Command; +use Illuminate\Support\Collection; +use Illuminate\Support\Str; + +use function Laravel\Prompts\progress; class CreateISPWebAccounts extends Command { - /** - * The name and signature of the console command. - * - * @var string - */ - protected $signature = 'ext:create-ispweb-accounts'; + protected $signature = 'ext:create-isp-accounts + {--dry-run : Simulate without creating accounts} + {--force : Force recreation even if client already exists}'; + + protected $description = 'Create ISPConfig clients for members and reassign their websites'; /** - * The console command description. - * - * @var string + * @throws Exception */ - protected $description = 'Créer les comptes ISPWeb des membres en fonction de leur domaine'; - - /** - * Execute the console command. - */ - public function handle() + public function handle(): int { - // + $isDryRun = $this->option('dry-run'); + $isForce = $this->option('force'); + + $this->info('ISPConfig Client Creation & Website Reassignment'); + + if ($isDryRun) { + $this->warn('DRY RUN MODE - No changes will be made'); + } + + $ispWeb = new ISPConfigWebService; + + // Récupération de tous les membres ayant un site web + $membersQuery = Member::whereNotNull('website_url') + ->where('website_url', '!=', ''); + + $totalMembers = $membersQuery->count(); + + if ($totalMembers === 0) { + $this->info('No members with websites found'); + + return self::SUCCESS; + } + + $this->info("Found {$totalMembers} members with websites"); + + // Récupération de tous les sites web depuis ISPConfig (avec cache) + $allWebsites = collect($ispWeb->getAllWebsites()); + + // Récupération des clients déjà créés dans Roxane (pour éviter les doublons) + $existingClients = IspconfigMember::where('type', IspconfigType::WEB) + ->whereIn('member_id', $membersQuery->pluck('id')) + ->get() + ->keyBy('member_id'); + + $progressBar = progress( + label: 'Processing members', + steps: $totalMembers + ); + + $progressBar->start(); + + // Statistiques de traitement + $stats = [ + 'clients_created' => 0, + 'websites_reassigned' => 0, + 'errors' => 0, + 'skipped' => 0, + ]; + + // Résultats détaillés pour le mode dry-run + $dryRunResults = []; + + // Traitement par lots de 50 membres + $membersQuery->chunk(50, function ($members) use ( + $ispWeb, + $allWebsites, + $existingClients, + $progressBar, + $isDryRun, + $isForce, + &$stats, + &$dryRunResults + ) { + foreach ($members as $member) { + try { + /** @var IspconfigMember|null $existingClient */ + $existingClient = $existingClients->get($member->id); + + // Extraction et normalisation des domaines depuis member->website_url + $memberDomains = $this->extractDomains($member->website_url); + + if ($memberDomains->isEmpty()) { + $this->warn("No valid domains for member: {$member->full_name}"); + $stats['skipped']++; + $progressBar->advance(); + + continue; + } + + // Recherche des sites web ISPConfig correspondant aux domaines du membre + $matchedWebsites = $ispWeb->findWebsitesForDomains($allWebsites, $memberDomains); + + if ($matchedWebsites->isEmpty()) { + $this->warn("No ISPConfig websites found for member: {$member->full_name}"); + $stats['skipped']++; + $progressBar->advance(); + + continue; + } + + $email = $member->retzien_email ?? $member->email; + $clientInfo = null; + + // Vérification 1 : Le client existe-t-il dans ISPConfig ? + $existingIspClient = $ispWeb->findClientByEmail($email); + + if ($existingIspClient && ! $isForce) { + // Client trouvé dans ISPConfig par email + $clientInfo = $existingIspClient; + $this->info("ISPConfig client found for {$member->full_name} with email {$email} (Client ID: {$clientInfo['client_id']})"); + } elseif ($existingClient && ! $isForce && $existingClient->ispconfig_client_id > 0) { + // Client déjà lié dans Roxane (fallback) - uniquement si client_id valide + $clientInfo = [ + 'client_id' => (int) $existingClient->ispconfig_client_id, + 'groupid' => (int) $existingClient->ispconfig_client_id, + ]; + $this->info("Member {$member->full_name} already linked to ISPConfig client (ID: {$clientInfo['client_id']})"); + } + + if (! $isDryRun) { + // MODE EXECUTION RÉELLE + + // Création du client ISPConfig si nécessaire + if ($clientInfo === null) { + $clientInfo = $this->createClient($member, $ispWeb, $email); + $stats['clients_created']++; + } + + // Réassignation de tous les sites web au client ISPConfig + foreach ($matchedWebsites as $website) { + $domainId = $website['domain_id']; + + $ispWeb->updateWebsiteClient($domainId, $clientInfo['groupid']); + $stats['websites_reassigned']++; + + $this->info("Reassigned {$website['domain']} to client {$clientInfo['client_id']}"); + } + + // Sauvegarde dans Roxane de la relation membre <-> client ISPConfig + $siteData = $matchedWebsites->map(fn ($site) => [ + 'domain_id' => $site['domain_id'], + 'domain' => $site['domain'], + ])->toArray(); + + IspconfigMember::updateOrCreate( + [ + 'member_id' => $member->id, + 'type' => IspconfigType::WEB, + ], + [ + 'ispconfig_client_id' => $clientInfo['client_id'], + 'data' => [ + 'sites' => $siteData, + ], + ] + ); + } else { + // MODE DRY-RUN : Collecte des informations sans modifications + + $willCreateClient = $clientInfo === null; + + if ($willCreateClient) { + $stats['clients_created']++; + } + + $dryRunResults[] = [ + 'member' => $member->full_name, + 'email' => $email, + 'action' => $willCreateClient ? 'CREATE ISP CLIENT' : 'USE EXISTING ISP CLIENT', + 'client_id' => $clientInfo['client_id'] ?? 'NEW', + 'websites' => $matchedWebsites->pluck('domain')->implode(', '), + 'sites_count' => $matchedWebsites->count(), + ]; + } + + } catch (Exception $e) { + $this->error("Error processing {$member->full_name}: {$e->getMessage()}"); + $stats['errors']++; + } + + $progressBar->advance(); + } + }); + + $progressBar->finish(); + + $this->newLine(); + $this->info('Summary:'); + $this->table( + ['Metric', 'Count'], + [ + ['Clients created', $stats['clients_created']], + ['Websites reassigned', $stats['websites_reassigned']], + ['Skipped', $stats['skipped']], + ['Errors', $stats['errors']], + ] + ); + + if ($isDryRun && ! empty($dryRunResults)) { + $this->newLine(); + $this->info('Dry Run Details:'); + $this->table( + ['Member', 'Email', 'Action', 'Client ID', 'Websites', 'Sites Count'], + collect($dryRunResults)->map(fn ($r) => [ + $r['member'], + $r['email'], + $r['action'], + $r['client_id'], + $r['websites'], + $r['sites_count'], + ]) + ); + } + + return $stats['errors'] > 0 ? self::FAILURE : self::SUCCESS; + } + + /** + * Extrait les domaines depuis la chaîne website_url (séparés par ;) + * et les normalise en minuscules + */ + private function extractDomains(string $websiteUrl): Collection + { + return collect(explode(';', $websiteUrl)) + ->map(fn ($url) => $this->normalizeDomain($url)) + ->filter() + ->unique() + ->values(); + } + + /** + * Normalise une URL pour extraire uniquement le nom de domaine + * Exemple: "https://www.example.com/path" → "www.example.com" + */ + private function normalizeDomain(string $url): ?string + { + $url = trim($url); + + if (! str_starts_with($url, 'http')) { + $url = 'https://'.$url; + } + + $host = parse_url($url, PHP_URL_HOST); + + return $host ? strtolower($host) : null; + } + + /** + * Crée un nouveau client dans ISPConfig avec toutes les données du membre + * + * @throws Exception + */ + private function createClient(Member $member, ISPConfigWebService $ispWeb, string $email): array + { + $username = $this->generateUsername($member); + $password = Str::random(16); + + // Préparation des données client pour ISPConfig + $clientData = [ + 'company_name' => $member->company ?? $member->full_name, + 'contact_name' => $member->full_name, + 'email' => $email, + 'username' => $username, + 'password' => $password, + 'customer_no' => (string) $member->id, + 'street' => $member->address ?? '', + 'zip' => $member->zipcode ?? '', + 'city' => $member->city ?? '', + 'country' => $member->country ?? 'FR', + 'telephone' => $member->phone1 ?? '', + 'mobile' => $member->phone2 ?? '', + 'internet' => $member->website_url ?? '', + ]; + + // Appel API ISPConfig pour créer le client + $clientInfo = $ispWeb->createClient($clientData); + + $this->info("Created ISPConfig client for {$member->full_name} (Client ID: {$clientInfo['client_id']})"); + + return $clientInfo; + } + + /** + * Génère un nom d'utilisateur pour ISPConfig + * Format: prenomnom (en minuscules, sans espaces ni accents) + * Exemple: "jeandurand", "johndoe" + */ + private function generateUsername(Member $member): string + { + $username = Str::slug($member->firstname.$member->lastname); + + return str_replace('-', '', $username); } } diff --git a/app/Console/Commands/SyncISPConfigWebMembers.php b/app/Console/Commands/SyncISPConfigWebMembers.php index 67a3212..87ccd58 100644 --- a/app/Console/Commands/SyncISPConfigWebMembers.php +++ b/app/Console/Commands/SyncISPConfigWebMembers.php @@ -7,11 +7,13 @@ use App\Models\IspconfigMember; use App\Models\Member; use App\Services\ISPConfig\ISPConfigWebService; use Illuminate\Console\Command; + use function Laravel\Prompts\progress; class SyncISPConfigWebMembers extends Command { protected $signature = 'sync:ispconfig-web-members {--refresh-cache : Vider le cache avant la synchronisation}'; + protected $description = 'Synchronise les services WEB ISPConfig des membres (via member->website_url)'; /** @@ -19,11 +21,9 @@ class SyncISPConfigWebMembers extends Command */ public function handle(): void { - //@todo: Retrouver le client_id pour chaque adhérent - $this->info('Synchronisation ISPConfig WEB (via member->website_url)'); - $ispWeb = new ISPConfigWebService(); + $ispWeb = new ISPConfigWebService; // Vider le cache si demandé if ($this->option('refresh-cache')) { @@ -60,38 +60,23 @@ class SyncISPConfigWebMembers extends Command // Extraction des domaines depuis website_url $memberDomains = collect(explode(';', $member->website_url)) - ->map(fn($url) => $this->normalizeDomain($url)) + ->map(fn ($url) => $this->normalizeDomain($url)) ->filter() ->unique() ->values(); if ($memberDomains->isEmpty()) { $progressBar->advance(); + continue; } // Recherche des sites ISPConfig correspondants - $matchedWebsites = $allWebsites->filter(function ($site) use ($memberDomains, $ispWeb) { - $siteDomain = strtolower($site['domain']); - - // Vérification du domaine principal - if ($memberDomains->contains($siteDomain)) { - return true; - } - - // Récupération et vérification des alias (avec cache) - $aliases = $ispWeb->getWebsiteAliases($site['domain_id']); - foreach ($aliases as $alias) { - if ($memberDomains->contains(strtolower($alias))) { - return true; - } - } - - return false; - }); + $matchedWebsites = $ispWeb->findWebsitesForDomains($allWebsites, $memberDomains); if ($matchedWebsites->isEmpty()) { $progressBar->advance(); + continue; } @@ -104,7 +89,7 @@ class SyncISPConfigWebMembers extends Command $ispWeb ) { $domainId = $site['domain_id']; - $sysGroupId = $site['sys_groupid']; + $sysGroupId = $site['sys_groupid'] ?? null; $domain = $site['domain']; // Récupération des alias (avec cache) @@ -112,8 +97,8 @@ class SyncISPConfigWebMembers extends Command // Filtrage des bases de données pour ce site $databases = $allDatabases - ->filter(fn($db) => $db['sys_groupid'] == $sysGroupId) - ->map(fn($db) => [ + ->filter(fn ($db) => $db['parent_domain_id'] == $domainId) + ->map(fn ($db) => [ 'database_id' => $db['database_id'], 'database_name' => $db['database_name'], 'database_user_id' => $db['database_user_id'], @@ -123,8 +108,8 @@ class SyncISPConfigWebMembers extends Command // Filtrage des utilisateurs FTP pour ce site $ftpUsers = $allFtpUsers - ->filter(fn($ftp) => $ftp['parent_domain_id'] == $domainId) - ->map(fn($ftp) => [ + ->filter(fn ($ftp) => $ftp['parent_domain_id'] == $domainId) + ->map(fn ($ftp) => [ 'ftp_user_id' => $ftp['ftp_user_id'], 'username' => $ftp['username'], 'dir' => $ftp['dir'], @@ -133,8 +118,8 @@ class SyncISPConfigWebMembers extends Command // Filtrage des utilisateurs Shell pour ce site $shellUsers = $allShellUsers - ->filter(fn($shell) => $shell['parent_domain_id'] == $domainId) - ->map(fn($shell) => [ + ->filter(fn ($shell) => $shell['parent_domain_id'] == $domainId) + ->map(fn ($shell) => [ 'shell_user_id' => $shell['shell_user_id'], 'username' => $shell['username'], 'shell' => $shell['shell'], @@ -163,7 +148,7 @@ class SyncISPConfigWebMembers extends Command return false; }) - ->map(fn($zone) => [ + ->map(fn ($zone) => [ 'id' => $zone['id'], 'origin' => $zone['origin'], 'ns' => $zone['ns'], @@ -176,6 +161,8 @@ class SyncISPConfigWebMembers extends Command return [ 'domain_id' => $domainId, 'domain' => $domain, + 'sys_groupid' => $sysGroupId, + 'system_group' => $site['system_group'] ?? null, 'document_root' => $site['document_root'], 'active' => $site['active'], 'aliases' => $aliases, @@ -195,6 +182,7 @@ class SyncISPConfigWebMembers extends Command 'ispconfig_service_user_id' => $siteData['domain_id'], ], [ + 'ispconfig_client_id' => $siteData['sys_groupid'], 'data' => $siteData, ] ); @@ -215,8 +203,8 @@ class SyncISPConfigWebMembers extends Command { $url = trim($url); - if (!str_starts_with($url, 'http')) { - $url = 'https://' . $url; + if (! str_starts_with($url, 'http')) { + $url = 'https://'.$url; } $host = parse_url($url, PHP_URL_HOST); diff --git a/app/Services/ISPConfig/ISPConfigWebService.php b/app/Services/ISPConfig/ISPConfigWebService.php index 568b1a5..b89bbd3 100644 --- a/app/Services/ISPConfig/ISPConfigWebService.php +++ b/app/Services/ISPConfig/ISPConfigWebService.php @@ -2,6 +2,7 @@ namespace App\Services\ISPConfig; +use Exception; use Illuminate\Support\Facades\Cache; class ISPConfigWebService extends ISPConfigService @@ -12,71 +13,69 @@ class ISPConfigWebService extends ISPConfigService } /** - * @throws \Exception + * @throws Exception */ public function getAllWebsites(): array { return Cache::remember( - "ispconfig.web.websites.all", + 'ispconfig.web.websites.all', config('services.ispconfig.cache_ttl'), - fn() => $this->call('sites_web_domain_get', ['primary_id' => -1]) + fn () => $this->call('sites_web_domain_get', ['primary_id' => -1]) ); } /** - * @throws \Exception + * @throws Exception */ public function getAllDatabases(): array { return Cache::remember( - "ispconfig.web.databases.all", + 'ispconfig.web.databases.all', config('services.ispconfig.cache_ttl'), - fn() => $this->call('sites_database_get', ['primary_id' => -1]) + fn () => $this->call('sites_database_get', ['primary_id' => -1]) ); } /** - * @throws \Exception + * @throws Exception */ public function getAllFtpUsers(): array { return Cache::remember( - "ispconfig.web.ftp.all", + 'ispconfig.web.ftp.all', config('services.ispconfig.cache_ttl'), - fn() => $this->call('sites_ftp_user_get', ['primary_id' => -1]) + fn () => $this->call('sites_ftp_user_get', ['primary_id' => -1]) ); } /** - * @throws \Exception + * @throws Exception */ public function getAllShellUsers(): array { return Cache::remember( - "ispconfig.web.shell.all", + 'ispconfig.web.shell.all', config('services.ispconfig.cache_ttl'), - fn() => $this->call('sites_shell_user_get', ['primary_id' => -1]) + fn () => $this->call('sites_shell_user_get', ['primary_id' => -1]) ); } /** - * @throws \Exception + * @throws Exception */ public function getAllDnsZones(): array { return Cache::remember( - "ispconfig.web.dns-zones.all", + 'ispconfig.web.dns-zones.all', config('services.ispconfig.cache_ttl'), - fn() => $this->call('dns_zone_get', ['primary_id' => -1]) + fn () => $this->call('dns_zone_get', ['primary_id' => -1]) ); } /** * Récupère la liste des alias d'un site web * - * @param int $domainId - * @return array - * @throws \Exception + * @throws Exception */ public function getWebsiteAliases(int $domainId): array { @@ -98,10 +97,12 @@ class ISPConfigWebService extends ISPConfigService } $aliases = array_map('trim', explode(',', $site['alias'])); - return array_values(array_filter($aliases, fn($alias) => !empty($alias))); - } catch (\Exception $e) { - \Log::error("Erreur lors de la récupération des alias pour le domaine {$domainId}: " . $e->getMessage()); + return array_values(array_filter($aliases, fn ($alias) => ! empty($alias))); + + } catch (Exception $e) { + \Log::error("Erreur lors de la récupération des alias pour le domaine {$domainId}: ".$e->getMessage()); + return []; } } @@ -111,9 +112,9 @@ class ISPConfigWebService extends ISPConfigService /** * Récupère la liste des bases de données d'un site en filtrant depuis toutes les BDD * - * @param int $sysGroupId - * @return array - * @throws \Exception + * @todo : utiliser plutôt domainId => parent_domain_id + * + * @throws Exception */ public function getWebsiteDatabases(int $sysGroupId): array { @@ -122,15 +123,15 @@ class ISPConfigWebService extends ISPConfigService // Filtrage par sys_groupid return collect($allDatabases) - ->filter(fn($db) => $db['sys_groupid'] == $sysGroupId) - ->map(fn($db) => [ + ->filter(fn ($db) => $db['sys_groupid'] == $sysGroupId) + ->map(fn ($db) => [ 'database_id' => $db['database_id'], 'database_name' => $db['database_name'], 'database_user' => $db['database_user'], 'database_type' => $db['type'], 'active' => $db['active'], 'remote_access' => $db['remote_access'], - 'remote_ips' => $db['remote_ips'] ?? '' + 'remote_ips' => $db['remote_ips'] ?? '', ]) ->values() ->toArray(); @@ -139,9 +140,7 @@ class ISPConfigWebService extends ISPConfigService /** * Récupère la liste des utilisateurs FTP d'un site en filtrant depuis tous les comptes FTP * - * @param int $domainId - * @return array - * @throws \Exception + * @throws Exception */ public function getWebsiteFtpUsers(int $domainId): array { @@ -150,15 +149,15 @@ class ISPConfigWebService extends ISPConfigService // Filtrage par parent_domain_id return collect($allFtpUsers) - ->filter(fn($ftp) => $ftp['parent_domain_id'] == $domainId) - ->map(fn($ftp) => [ + ->filter(fn ($ftp) => $ftp['parent_domain_id'] == $domainId) + ->map(fn ($ftp) => [ 'ftp_user_id' => $ftp['ftp_user_id'], 'username' => $ftp['username'], 'dir' => $ftp['dir'], 'quota_size' => $ftp['quota_size'], 'active' => $ftp['active'], 'uid' => $ftp['uid'], - 'gid' => $ftp['gid'] + 'gid' => $ftp['gid'], ]) ->values() ->toArray(); @@ -167,9 +166,7 @@ class ISPConfigWebService extends ISPConfigService /** * Récupère la liste des utilisateurs Shell d'un site en filtrant depuis tous les comptes Shell * - * @param int $domainId - * @return array - * @throws \Exception + * @throws Exception */ public function getWebsiteShellUsers(int $domainId): array { @@ -178,8 +175,8 @@ class ISPConfigWebService extends ISPConfigService // Filtrage par parent_domain_id return collect($allShellUsers) - ->filter(fn($shell) => $shell['parent_domain_id'] == $domainId) - ->map(fn($shell) => [ + ->filter(fn ($shell) => $shell['parent_domain_id'] == $domainId) + ->map(fn ($shell) => [ 'shell_user_id' => $shell['shell_user_id'], 'username' => $shell['username'], 'dir' => $shell['dir'], @@ -189,7 +186,7 @@ class ISPConfigWebService extends ISPConfigService 'quota_size' => $shell['quota_size'], 'active' => $shell['active'], 'chroot' => $shell['chroot'], - 'ssh_rsa' => !empty($shell['ssh_rsa']) + 'ssh_rsa' => ! empty($shell['ssh_rsa']), ]) ->values() ->toArray(); @@ -198,9 +195,7 @@ class ISPConfigWebService extends ISPConfigService /** * Récupère toutes les informations complètes d'un site (alias, BDD, FTP, Shell) * - * @param int $domainId - * @return array|null - * @throws \Exception + * @throws Exception */ public function getWebsiteCompleteInfo(int $domainId): ?array { @@ -218,7 +213,7 @@ class ISPConfigWebService extends ISPConfigService // Récupérer les alias $aliases = []; - if (!empty($site['alias'])) { + if (! empty($site['alias'])) { $aliases = array_values(array_filter(array_map('trim', explode(',', $site['alias'])))); } @@ -231,7 +226,7 @@ class ISPConfigWebService extends ISPConfigService 'aliases' => $aliases, 'databases' => $this->getWebsiteDatabases($domainId, $site['sys_groupid']), 'ftp_users' => $this->getWebsiteFtpUsers($domainId), - 'shell_users' => $this->getWebsiteShellUsers($domainId) + 'shell_users' => $this->getWebsiteShellUsers($domainId), ]; } ); @@ -239,9 +234,6 @@ class ISPConfigWebService extends ISPConfigService /** * Vide le cache pour un domaine spécifique - * - * @param int $domainId - * @return void */ public function clearDomainCache(int $domainId): void { @@ -251,16 +243,177 @@ class ISPConfigWebService extends ISPConfigService /** * Vide tout le cache ISPConfig Web - * - * @return void */ public function clearAllCache(): void { - Cache::forget("ispconfig.web.websites.all"); - Cache::forget("ispconfig.web.databases.all"); - Cache::forget("ispconfig.web.ftp.all"); - Cache::forget("ispconfig.web.shell.all"); - Cache::forget("ispconfig.web.dns-zones.all"); - Cache::forget("ispconfig.web.domain-alias.all"); + Cache::forget('ispconfig.web.websites.all'); + Cache::forget('ispconfig.web.databases.all'); + Cache::forget('ispconfig.web.ftp.all'); + Cache::forget('ispconfig.web.shell.all'); + Cache::forget('ispconfig.web.dns-zones.all'); + Cache::forget('ispconfig.web.domain-alias.all'); + } + + /** + * Find websites matching given domains (including aliases) + */ + public function findWebsitesForDomains(\Illuminate\Support\Collection $allWebsites, \Illuminate\Support\Collection $domains): \Illuminate\Support\Collection + { + return $allWebsites->filter(function ($site) use ($domains) { + $siteDomain = strtolower($site['domain']); + + if ($domains->contains($siteDomain)) { + return true; + } + + $aliases = $this->getWebsiteAliases($site['domain_id']); + foreach ($aliases as $alias) { + if ($domains->contains(strtolower($alias))) { + return true; + } + } + + return false; + }); + } + + /** + * Find a client by email using sys_groupid from existing websites + * Since ISPConfig API doesn't provide a direct way to search clients by email, + * we search through websites to find the sys_groupid (client group) + * + * @throws Exception + */ + public function findClientByEmail(string $email): ?array + { + $allWebsites = $this->getAllWebsites(); + $normalizedEmail = strtolower(trim($email)); + + foreach ($allWebsites as $website) { + $sysGroupId = $website['sys_groupid'] ?? null; + $systemGroup = $website['system_group'] ?? null; + + if (! $sysGroupId || ! $systemGroup) { + continue; + } + + try { + $clientData = $this->call('client_get_by_groupid', [$sysGroupId]); + + if (! empty($clientData) && isset($clientData['email'])) { + if (strtolower(trim($clientData['email'])) === $normalizedEmail) { + return [ + 'client_id' => $clientData['client_id'] ?? $sysGroupId, + 'groupid' => $sysGroupId, + 'system_group' => $systemGroup, + 'email' => $clientData['email'], + ]; + } + } + } catch (Exception $e) { + continue; + } + } + + return null; + } + + /** + * Create a new ISPConfig client + * Returns an array with client_id and groupid + * + * @throws Exception + */ + public function createClient(array $clientData): array + { + $resellerId = 1; + + $defaultParams = [ + 'limit_maildomain' => -1, + 'limit_mailbox' => -1, + 'limit_mailalias' => -1, + 'limit_mailaliasdomain' => -1, + 'limit_mailforward' => -1, + 'limit_mailcatchall' => -1, + 'limit_mailrouting' => 0, + 'limit_mail_wblist' => 0, + 'limit_mailfilter' => -1, + 'limit_fetchmail' => -1, + 'limit_mailquota' => -1, + 'limit_spamfilter_wblist' => 0, + 'limit_spamfilter_user' => 0, + 'limit_spamfilter_policy' => 1, + 'default_webserver' => 1, + 'limit_web_ip' => '', + 'limit_web_domain' => -1, + 'limit_web_quota' => -1, + 'web_php_options' => 'no,fast-cgi,cgi,mod,suphp', + 'limit_web_subdomain' => -1, + 'limit_web_aliasdomain' => -1, + 'limit_ftp_user' => -1, + 'limit_shell_user' => 0, + 'ssh_chroot' => 'no,jailkit,ssh-chroot', + 'limit_webdav_user' => 0, + 'default_dnsserver' => 1, + 'limit_dns_zone' => -1, + 'limit_dns_slave_zone' => -1, + 'limit_dns_record' => -1, + 'default_dbserver' => 1, + 'limit_database' => -1, + 'limit_cron' => 0, + 'limit_cron_type' => 'url', + 'limit_cron_frequency' => 5, + 'limit_traffic_quota' => -1, + 'limit_client' => 0, + 'parent_client_id' => 0, + 'language' => 'fr', + 'usertheme' => 'default', + 'template_master' => 0, + 'template_additional' => '', + 'created_at' => 0, + 'default_mailserver' => 1, + ]; + + $params = array_merge($defaultParams, $clientData); + + $clientId = $this->call('client_add', [$resellerId, $params]); + + $client = $this->call('client_get', [(int) $clientId]); + + $this->clearAllCache(); + + return [ + 'client_id' => (int) $clientId, + 'groupid' => (int) ($client['groupid'] ?? $clientId), + 'system_group' => 'client'.$clientId, + ]; + } + + /** + * Update website to assign it to a different client + * + * @throws Exception + */ + public function updateWebsiteClient(int $domainId, int $newClientId): bool + { + $websiteRecord = $this->call('sites_web_domain_get', ['domain_id' => $domainId]); + + if (! is_array($websiteRecord) || empty($websiteRecord)) { + throw new \RuntimeException("Website with domain_id {$domainId} not found"); + } + + // sys_groupid n'est pas trouvable dans les données client + // $websiteRecord['sys_groupid'] = $newClientId; + $websiteRecord['system_group'] = 'client'.$newClientId; + + $result = $this->call('sites_web_domain_update', [ + 0, + $domainId, + $websiteRecord, + ]); + + $this->clearAllCache(); + + return (bool) $result; } } diff --git a/tests/Feature/Commands/CreateISPWebAccountsTest.php b/tests/Feature/Commands/CreateISPWebAccountsTest.php new file mode 100644 index 0000000..7d4e0b6 --- /dev/null +++ b/tests/Feature/Commands/CreateISPWebAccountsTest.php @@ -0,0 +1,68 @@ +artisan('ext:create-isp-accounts') + ->expectsOutput('No members with websites found') + ->assertExitCode(0); + } + + public function test_command_processes_members_regardless_of_status(): void + { + Member::factory()->create([ + 'status' => 'draft', + 'website_url' => 'example.com', + 'firstname' => 'John', + 'lastname' => 'Doe', + ]); + + $this->artisan('ext:create-isp-accounts --dry-run') + ->expectsOutput('DRY RUN MODE - No changes will be made') + ->assertExitCode(0); + } + + public function test_command_runs_in_dry_run_mode(): void + { + Member::factory()->create([ + 'status' => 'valid', + 'website_url' => 'example.com', + 'firstname' => 'John', + 'lastname' => 'Doe', + ]); + + $this->artisan('ext:create-isp-accounts --dry-run') + ->expectsOutput('DRY RUN MODE - No changes will be made') + ->assertExitCode(0); + } + + public function test_command_skips_member_with_existing_client(): void + { + $member = Member::factory()->create([ + 'status' => 'valid', + 'website_url' => 'example.com', + ]); + + IspconfigMember::create([ + 'member_id' => $member->id, + 'type' => IspconfigType::WEB, + 'ispconfig_client_id' => '123', + ]); + + $this->artisan('ext:create-isp-accounts --dry-run') + ->assertExitCode(0); + + $this->assertDatabaseCount('ispconfigs_members', 1); + } +}