feat(ISPConfig Webhosting): add creation script for members with website
All checks were successful
Deploy Roxane to Preprod / deploy (push) Successful in 1m23s

This commit is contained in:
2026-08-19 18:31:02 +02:00
parent 7dff670122
commit 98893924ac
5 changed files with 596 additions and 104 deletions

View File

@@ -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 | | MemberService | Inscription et desactivation de membres |
| DolibarrService | Integration ERP via API REST | | DolibarrService | Integration ERP via API REST |
| ISPConfigMailService | Gestion comptes mail via SOAP | | 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) | | 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` | | `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-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 | | `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 | | `nextcloud:sync-members` | Lie les membres a leurs comptes Nextcloud |
| `memberships:sync-services` | Synchronise les services associes aux membres | | `memberships:sync-services` | Synchronise les services associes aux membres |

View File

@@ -2,29 +2,311 @@
namespace App\Console\Commands; 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\Console\Command;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use function Laravel\Prompts\progress;
class CreateISPWebAccounts extends Command class CreateISPWebAccounts extends Command
{ {
/** protected $signature = 'ext:create-isp-accounts
* The name and signature of the console command. {--dry-run : Simulate without creating accounts}
* {--force : Force recreation even if client already exists}';
* @var string
*/ protected $description = 'Create ISPConfig clients for members and reassign their websites';
protected $signature = 'ext:create-ispweb-accounts';
/** /**
* The console command description. * @throws Exception
*
* @var string
*/ */
protected $description = 'Créer les comptes ISPWeb des membres en fonction de leur domaine'; public function handle(): int
/**
* Execute the console command.
*/
public function handle()
{ {
// $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);
} }
} }

View File

@@ -7,11 +7,13 @@ use App\Models\IspconfigMember;
use App\Models\Member; use App\Models\Member;
use App\Services\ISPConfig\ISPConfigWebService; use App\Services\ISPConfig\ISPConfigWebService;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use function Laravel\Prompts\progress; use function Laravel\Prompts\progress;
class SyncISPConfigWebMembers extends Command class SyncISPConfigWebMembers extends Command
{ {
protected $signature = 'sync:ispconfig-web-members {--refresh-cache : Vider le cache avant la synchronisation}'; 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)'; 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 public function handle(): void
{ {
//@todo: Retrouver le client_id pour chaque adhérent
$this->info('Synchronisation ISPConfig WEB (via member->website_url)'); $this->info('Synchronisation ISPConfig WEB (via member->website_url)');
$ispWeb = new ISPConfigWebService(); $ispWeb = new ISPConfigWebService;
// Vider le cache si demandé // Vider le cache si demandé
if ($this->option('refresh-cache')) { if ($this->option('refresh-cache')) {
@@ -60,38 +60,23 @@ class SyncISPConfigWebMembers extends Command
// Extraction des domaines depuis website_url // Extraction des domaines depuis website_url
$memberDomains = collect(explode(';', $member->website_url)) $memberDomains = collect(explode(';', $member->website_url))
->map(fn($url) => $this->normalizeDomain($url)) ->map(fn ($url) => $this->normalizeDomain($url))
->filter() ->filter()
->unique() ->unique()
->values(); ->values();
if ($memberDomains->isEmpty()) { if ($memberDomains->isEmpty()) {
$progressBar->advance(); $progressBar->advance();
continue; continue;
} }
// Recherche des sites ISPConfig correspondants // Recherche des sites ISPConfig correspondants
$matchedWebsites = $allWebsites->filter(function ($site) use ($memberDomains, $ispWeb) { $matchedWebsites = $ispWeb->findWebsitesForDomains($allWebsites, $memberDomains);
$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;
});
if ($matchedWebsites->isEmpty()) { if ($matchedWebsites->isEmpty()) {
$progressBar->advance(); $progressBar->advance();
continue; continue;
} }
@@ -104,7 +89,7 @@ class SyncISPConfigWebMembers extends Command
$ispWeb $ispWeb
) { ) {
$domainId = $site['domain_id']; $domainId = $site['domain_id'];
$sysGroupId = $site['sys_groupid']; $sysGroupId = $site['sys_groupid'] ?? null;
$domain = $site['domain']; $domain = $site['domain'];
// Récupération des alias (avec cache) // Récupération des alias (avec cache)
@@ -112,8 +97,8 @@ class SyncISPConfigWebMembers extends Command
// Filtrage des bases de données pour ce site // Filtrage des bases de données pour ce site
$databases = $allDatabases $databases = $allDatabases
->filter(fn($db) => $db['sys_groupid'] == $sysGroupId) ->filter(fn ($db) => $db['parent_domain_id'] == $domainId)
->map(fn($db) => [ ->map(fn ($db) => [
'database_id' => $db['database_id'], 'database_id' => $db['database_id'],
'database_name' => $db['database_name'], 'database_name' => $db['database_name'],
'database_user_id' => $db['database_user_id'], 'database_user_id' => $db['database_user_id'],
@@ -123,8 +108,8 @@ class SyncISPConfigWebMembers extends Command
// Filtrage des utilisateurs FTP pour ce site // Filtrage des utilisateurs FTP pour ce site
$ftpUsers = $allFtpUsers $ftpUsers = $allFtpUsers
->filter(fn($ftp) => $ftp['parent_domain_id'] == $domainId) ->filter(fn ($ftp) => $ftp['parent_domain_id'] == $domainId)
->map(fn($ftp) => [ ->map(fn ($ftp) => [
'ftp_user_id' => $ftp['ftp_user_id'], 'ftp_user_id' => $ftp['ftp_user_id'],
'username' => $ftp['username'], 'username' => $ftp['username'],
'dir' => $ftp['dir'], 'dir' => $ftp['dir'],
@@ -133,8 +118,8 @@ class SyncISPConfigWebMembers extends Command
// Filtrage des utilisateurs Shell pour ce site // Filtrage des utilisateurs Shell pour ce site
$shellUsers = $allShellUsers $shellUsers = $allShellUsers
->filter(fn($shell) => $shell['parent_domain_id'] == $domainId) ->filter(fn ($shell) => $shell['parent_domain_id'] == $domainId)
->map(fn($shell) => [ ->map(fn ($shell) => [
'shell_user_id' => $shell['shell_user_id'], 'shell_user_id' => $shell['shell_user_id'],
'username' => $shell['username'], 'username' => $shell['username'],
'shell' => $shell['shell'], 'shell' => $shell['shell'],
@@ -163,7 +148,7 @@ class SyncISPConfigWebMembers extends Command
return false; return false;
}) })
->map(fn($zone) => [ ->map(fn ($zone) => [
'id' => $zone['id'], 'id' => $zone['id'],
'origin' => $zone['origin'], 'origin' => $zone['origin'],
'ns' => $zone['ns'], 'ns' => $zone['ns'],
@@ -176,6 +161,8 @@ class SyncISPConfigWebMembers extends Command
return [ return [
'domain_id' => $domainId, 'domain_id' => $domainId,
'domain' => $domain, 'domain' => $domain,
'sys_groupid' => $sysGroupId,
'system_group' => $site['system_group'] ?? null,
'document_root' => $site['document_root'], 'document_root' => $site['document_root'],
'active' => $site['active'], 'active' => $site['active'],
'aliases' => $aliases, 'aliases' => $aliases,
@@ -195,6 +182,7 @@ class SyncISPConfigWebMembers extends Command
'ispconfig_service_user_id' => $siteData['domain_id'], 'ispconfig_service_user_id' => $siteData['domain_id'],
], ],
[ [
'ispconfig_client_id' => $siteData['sys_groupid'],
'data' => $siteData, 'data' => $siteData,
] ]
); );
@@ -215,8 +203,8 @@ class SyncISPConfigWebMembers extends Command
{ {
$url = trim($url); $url = trim($url);
if (!str_starts_with($url, 'http')) { if (! str_starts_with($url, 'http')) {
$url = 'https://' . $url; $url = 'https://'.$url;
} }
$host = parse_url($url, PHP_URL_HOST); $host = parse_url($url, PHP_URL_HOST);

View File

@@ -2,6 +2,7 @@
namespace App\Services\ISPConfig; namespace App\Services\ISPConfig;
use Exception;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
class ISPConfigWebService extends ISPConfigService class ISPConfigWebService extends ISPConfigService
@@ -12,71 +13,69 @@ class ISPConfigWebService extends ISPConfigService
} }
/** /**
* @throws \Exception * @throws Exception
*/ */
public function getAllWebsites(): array public function getAllWebsites(): array
{ {
return Cache::remember( return Cache::remember(
"ispconfig.web.websites.all", 'ispconfig.web.websites.all',
config('services.ispconfig.cache_ttl'), 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 public function getAllDatabases(): array
{ {
return Cache::remember( return Cache::remember(
"ispconfig.web.databases.all", 'ispconfig.web.databases.all',
config('services.ispconfig.cache_ttl'), 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 public function getAllFtpUsers(): array
{ {
return Cache::remember( return Cache::remember(
"ispconfig.web.ftp.all", 'ispconfig.web.ftp.all',
config('services.ispconfig.cache_ttl'), 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 public function getAllShellUsers(): array
{ {
return Cache::remember( return Cache::remember(
"ispconfig.web.shell.all", 'ispconfig.web.shell.all',
config('services.ispconfig.cache_ttl'), 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 public function getAllDnsZones(): array
{ {
return Cache::remember( return Cache::remember(
"ispconfig.web.dns-zones.all", 'ispconfig.web.dns-zones.all',
config('services.ispconfig.cache_ttl'), 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 * Récupère la liste des alias d'un site web
* *
* @param int $domainId * @throws Exception
* @return array
* @throws \Exception
*/ */
public function getWebsiteAliases(int $domainId): array public function getWebsiteAliases(int $domainId): array
{ {
@@ -98,10 +97,12 @@ class ISPConfigWebService extends ISPConfigService
} }
$aliases = array_map('trim', explode(',', $site['alias'])); $aliases = array_map('trim', explode(',', $site['alias']));
return array_values(array_filter($aliases, fn($alias) => !empty($alias)));
} catch (\Exception $e) { return array_values(array_filter($aliases, fn ($alias) => ! empty($alias)));
\Log::error("Erreur lors de la récupération des alias pour le domaine {$domainId}: " . $e->getMessage());
} catch (Exception $e) {
\Log::error("Erreur lors de la récupération des alias pour le domaine {$domainId}: ".$e->getMessage());
return []; 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 * Récupère la liste des bases de données d'un site en filtrant depuis toutes les BDD
* *
* @param int $sysGroupId * @todo : utiliser plutôt domainId => parent_domain_id
* @return array *
* @throws \Exception * @throws Exception
*/ */
public function getWebsiteDatabases(int $sysGroupId): array public function getWebsiteDatabases(int $sysGroupId): array
{ {
@@ -122,15 +123,15 @@ class ISPConfigWebService extends ISPConfigService
// Filtrage par sys_groupid // Filtrage par sys_groupid
return collect($allDatabases) return collect($allDatabases)
->filter(fn($db) => $db['sys_groupid'] == $sysGroupId) ->filter(fn ($db) => $db['sys_groupid'] == $sysGroupId)
->map(fn($db) => [ ->map(fn ($db) => [
'database_id' => $db['database_id'], 'database_id' => $db['database_id'],
'database_name' => $db['database_name'], 'database_name' => $db['database_name'],
'database_user' => $db['database_user'], 'database_user' => $db['database_user'],
'database_type' => $db['type'], 'database_type' => $db['type'],
'active' => $db['active'], 'active' => $db['active'],
'remote_access' => $db['remote_access'], 'remote_access' => $db['remote_access'],
'remote_ips' => $db['remote_ips'] ?? '' 'remote_ips' => $db['remote_ips'] ?? '',
]) ])
->values() ->values()
->toArray(); ->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 * Récupère la liste des utilisateurs FTP d'un site en filtrant depuis tous les comptes FTP
* *
* @param int $domainId * @throws Exception
* @return array
* @throws \Exception
*/ */
public function getWebsiteFtpUsers(int $domainId): array public function getWebsiteFtpUsers(int $domainId): array
{ {
@@ -150,15 +149,15 @@ class ISPConfigWebService extends ISPConfigService
// Filtrage par parent_domain_id // Filtrage par parent_domain_id
return collect($allFtpUsers) return collect($allFtpUsers)
->filter(fn($ftp) => $ftp['parent_domain_id'] == $domainId) ->filter(fn ($ftp) => $ftp['parent_domain_id'] == $domainId)
->map(fn($ftp) => [ ->map(fn ($ftp) => [
'ftp_user_id' => $ftp['ftp_user_id'], 'ftp_user_id' => $ftp['ftp_user_id'],
'username' => $ftp['username'], 'username' => $ftp['username'],
'dir' => $ftp['dir'], 'dir' => $ftp['dir'],
'quota_size' => $ftp['quota_size'], 'quota_size' => $ftp['quota_size'],
'active' => $ftp['active'], 'active' => $ftp['active'],
'uid' => $ftp['uid'], 'uid' => $ftp['uid'],
'gid' => $ftp['gid'] 'gid' => $ftp['gid'],
]) ])
->values() ->values()
->toArray(); ->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 * Récupère la liste des utilisateurs Shell d'un site en filtrant depuis tous les comptes Shell
* *
* @param int $domainId * @throws Exception
* @return array
* @throws \Exception
*/ */
public function getWebsiteShellUsers(int $domainId): array public function getWebsiteShellUsers(int $domainId): array
{ {
@@ -178,8 +175,8 @@ class ISPConfigWebService extends ISPConfigService
// Filtrage par parent_domain_id // Filtrage par parent_domain_id
return collect($allShellUsers) return collect($allShellUsers)
->filter(fn($shell) => $shell['parent_domain_id'] == $domainId) ->filter(fn ($shell) => $shell['parent_domain_id'] == $domainId)
->map(fn($shell) => [ ->map(fn ($shell) => [
'shell_user_id' => $shell['shell_user_id'], 'shell_user_id' => $shell['shell_user_id'],
'username' => $shell['username'], 'username' => $shell['username'],
'dir' => $shell['dir'], 'dir' => $shell['dir'],
@@ -189,7 +186,7 @@ class ISPConfigWebService extends ISPConfigService
'quota_size' => $shell['quota_size'], 'quota_size' => $shell['quota_size'],
'active' => $shell['active'], 'active' => $shell['active'],
'chroot' => $shell['chroot'], 'chroot' => $shell['chroot'],
'ssh_rsa' => !empty($shell['ssh_rsa']) 'ssh_rsa' => ! empty($shell['ssh_rsa']),
]) ])
->values() ->values()
->toArray(); ->toArray();
@@ -198,9 +195,7 @@ class ISPConfigWebService extends ISPConfigService
/** /**
* Récupère toutes les informations complètes d'un site (alias, BDD, FTP, Shell) * Récupère toutes les informations complètes d'un site (alias, BDD, FTP, Shell)
* *
* @param int $domainId * @throws Exception
* @return array|null
* @throws \Exception
*/ */
public function getWebsiteCompleteInfo(int $domainId): ?array public function getWebsiteCompleteInfo(int $domainId): ?array
{ {
@@ -218,7 +213,7 @@ class ISPConfigWebService extends ISPConfigService
// Récupérer les alias // Récupérer les alias
$aliases = []; $aliases = [];
if (!empty($site['alias'])) { if (! empty($site['alias'])) {
$aliases = array_values(array_filter(array_map('trim', explode(',', $site['alias'])))); $aliases = array_values(array_filter(array_map('trim', explode(',', $site['alias']))));
} }
@@ -231,7 +226,7 @@ class ISPConfigWebService extends ISPConfigService
'aliases' => $aliases, 'aliases' => $aliases,
'databases' => $this->getWebsiteDatabases($domainId, $site['sys_groupid']), 'databases' => $this->getWebsiteDatabases($domainId, $site['sys_groupid']),
'ftp_users' => $this->getWebsiteFtpUsers($domainId), '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 * Vide le cache pour un domaine spécifique
*
* @param int $domainId
* @return void
*/ */
public function clearDomainCache(int $domainId): void public function clearDomainCache(int $domainId): void
{ {
@@ -251,16 +243,177 @@ class ISPConfigWebService extends ISPConfigService
/** /**
* Vide tout le cache ISPConfig Web * Vide tout le cache ISPConfig Web
*
* @return void
*/ */
public function clearAllCache(): void public function clearAllCache(): void
{ {
Cache::forget("ispconfig.web.websites.all"); Cache::forget('ispconfig.web.websites.all');
Cache::forget("ispconfig.web.databases.all"); Cache::forget('ispconfig.web.databases.all');
Cache::forget("ispconfig.web.ftp.all"); Cache::forget('ispconfig.web.ftp.all');
Cache::forget("ispconfig.web.shell.all"); Cache::forget('ispconfig.web.shell.all');
Cache::forget("ispconfig.web.dns-zones.all"); Cache::forget('ispconfig.web.dns-zones.all');
Cache::forget("ispconfig.web.domain-alias.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;
} }
} }

View File

@@ -0,0 +1,68 @@
<?php
namespace Tests\Feature\Commands;
use App\Enums\IspconfigType;
use App\Models\IspconfigMember;
use App\Models\Member;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class CreateISPWebAccountsTest extends TestCase
{
use RefreshDatabase;
public function test_command_exits_successfully_when_no_members_with_websites(): void
{
$this->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);
}
}