diff --git a/app/Console/Commands/SyncISPConfigMailMembers.php b/app/Console/Commands/SyncISPConfigMailMembers.php new file mode 100644 index 0000000..25ef14d --- /dev/null +++ b/app/Console/Commands/SyncISPConfigMailMembers.php @@ -0,0 +1,89 @@ +info('Synchronisation ISPConfig MAIL'); + + $ispMail = new ISPConfigMailService(); + + //Récupération de tous les mail users + $mailUsers = collect($ispMail->getAllMailUsers()); + + $progressBar = progress(label: 'ISPConfig Mail Members import', steps: $mailUsers->count()); + $progressBar->start(); + + $emailToMailUserId = $mailUsers + ->filter(fn ($u) => isset($u['email'], $u['mailuser_id'])) + ->mapWithKeys(fn ($u) => [ + strtolower($u['email']) => (int) $u['mailuser_id'], + ]); + + $synced = 0; + + // Parcours des membres + Member::whereNotNull('email')->chunk(100, function ($members) use ( + $progressBar, + $emailToMailUserId, + $ispMail, + &$synced + ) { + foreach ($members as $member) { + $emails = array_map('trim', explode(';', $member->email)); + + $retzienEmail = collect($emails) + ->map(fn ($e) => strtolower($e)) + ->first(fn ($e) => str_ends_with($e, '@retzien.fr')); + + if (!$retzienEmail) { + continue; + } + + $mailUserId = $emailToMailUserId->get($retzienEmail); + + if (!$mailUserId) { + $this->warn("Aucun mail user ISPConfig pour {$retzienEmail}"); + continue; + } + + //Récupération des données complètes de la boîte mail + $mailUserData = $ispMail->getMailUserDetails($retzienEmail); + + // Création / mise à jour + IspconfigMember::updateOrCreate( + [ + 'member_id' => $member->id, + //@todo : 'ispconfig_client_id' => ?, + 'type' => IspconfigType::MAIL, + 'email' => $retzienEmail, + ], + [ + 'ispconfig_service_user_id' => $mailUserId, + 'data' => [ + // @todo: traiter plus tard le cas de plusieurs mail pour un adhérent + 'mailuser' => [$mailUserData], + ], + ] + ); + $synced++; + $progressBar->advance(); + } + }); + + $progressBar->finish(); + $this->info("{$synced} services mail synchronisés"); + } +} diff --git a/app/Console/Commands/SyncISPConfigMembersCommand.php b/app/Console/Commands/SyncISPConfigMembersCommand.php deleted file mode 100644 index 0afe3c3..0000000 --- a/app/Console/Commands/SyncISPConfigMembersCommand.php +++ /dev/null @@ -1,112 +0,0 @@ -info('Début de la synchronisation ISPConfig Mail'); - - $ispConfigMailService = new ISPConfigMailService(); - - // TESTS - /*$ispConfigWebService = new ISPConfigWebService(); - - $webClients = []; - - $allWebClients = collect($ispConfigWebService->getAllClients()); - - foreach ($allWebClients as $wclientId) - { - $webClients[] = $ispConfigWebService->getClientData($wclientId); - } - - dd($webClients); - - $allClients = collect($ispConfigMailService->getAllClients()); - - $clients = []; - - foreach ($allClients as $clientId) { - $clients[] = $ispConfigMailService->getClientData($clientId); - } - - dd($clients);*/ - - // Récupération de tous les utilisateurs mail ISPConfig - $mailUsers = collect($ispConfigMailService->getAllMailUsers()); - - dd($mailUsers); - // Construction d'une map email => mailuser_id (!= client_id car indispo via API ISP) - $emailToClientId = $mailUsers - ->filter(fn ($user) => isset($user['email'], $user['mailuser_id'])) - ->mapWithKeys(fn ($user) => [ - strtolower($user['email']) => (int) $user['mailuser_id'] - ]); - - dd($emailToClientId); - - // Tableau des changements - $membersAddedOrUpdated = []; - - // Parcours des members - Member::query() - ->whereNotNull('email') - ->chunk(100, function ($members) use ($emailToClientId) { - foreach ($members as $member) { - - // Emails séparés par ; - $emails = array_map('trim', explode(';', $member->email)); - - // On récupère uniquement l'email @retzien.fr - $retzienEmail = collect($emails) - ->first(fn ($email) => str_ends_with(strtolower($email), '@retzien.fr')); - - if (!$retzienEmail) { - continue; - } - - $retzienEmail = strtolower($retzienEmail); - - // Recherche du client ISPConfig correspondant - $clientId = $emailToClientId->get($retzienEmail); - - if (!$clientId) { - $this->warn("Client ISPConfig non trouvé pour {$retzienEmail}"); - continue; - } - - // Mise à jour si nécessaire - if ($member->ispconfig_mail_client_id !== $clientId) { - - // Debug => Ajout au tableau des modifs - $membersAddedOrUpdated[] = [ - 'member_id' => $member->id, - 'isp_id' => $clientId - ]; - - //$member->update([ - //'ispconfig_mail_client_id' => $clientId, - //]); - - //$this->info("Member {$member->id} synchronisé → client ISPConfig {$clientId}"); - } - } - }); - - // Debug - dd($membersAddedOrUpdated); - - $this->info('Synchronisation ISPConfig Mail terminée'); - } -} diff --git a/app/Console/Commands/SyncISPConfigWebMembers.php b/app/Console/Commands/SyncISPConfigWebMembers.php new file mode 100644 index 0000000..67a3212 --- /dev/null +++ b/app/Console/Commands/SyncISPConfigWebMembers.php @@ -0,0 +1,226 @@ +website_url)'; + + /** + * @throws \Exception + */ + 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(); + + // Vider le cache si demandé + if ($this->option('refresh-cache')) { + $this->info('Vidage du cache ISPConfig...'); + $ispWeb->clearAllCache(); + } + + // Récupération de toutes les données ISPConfig en une seule fois (avec cache) + $this->info('Chargement des données ISPConfig...'); + $allWebsites = collect($ispWeb->getAllWebsites()); + $allDatabases = collect($ispWeb->getAllDatabases()); + $allFtpUsers = collect($ispWeb->getAllFtpUsers()); + $allShellUsers = collect($ispWeb->getAllShellUsers()); + $allDnsZones = collect($ispWeb->getAllDnsZones()); + + $progressBar = progress( + label: 'ISPConfig Web Members import', + steps: Member::whereNotNull('website_url')->count() + ); + + $progressBar->start(); + + // Parcours des membres + Member::whereNotNull('website_url')->chunk(100, function ($members) use ( + $allWebsites, + $allDatabases, + $allFtpUsers, + $allShellUsers, + $allDnsZones, + $ispWeb, + $progressBar + ) { + foreach ($members as $member) { + + // Extraction des domaines depuis website_url + $memberDomains = collect(explode(';', $member->website_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; + }); + + if ($matchedWebsites->isEmpty()) { + $progressBar->advance(); + continue; + } + + // Construction des données pour chaque site + $sitesData = $matchedWebsites->map(function ($site) use ( + $allDatabases, + $allFtpUsers, + $allShellUsers, + $allDnsZones, + $ispWeb + ) { + $domainId = $site['domain_id']; + $sysGroupId = $site['sys_groupid']; + $domain = $site['domain']; + + // Récupération des alias (avec cache) + $aliases = $ispWeb->getWebsiteAliases($domainId); + + // Filtrage des bases de données pour ce site + $databases = $allDatabases + ->filter(fn($db) => $db['sys_groupid'] == $sysGroupId) + ->map(fn($db) => [ + 'database_id' => $db['database_id'], + 'database_name' => $db['database_name'], + 'database_user_id' => $db['database_user_id'], + 'database_type' => $db['type'], + ]) + ->values(); + + // Filtrage des utilisateurs FTP pour ce site + $ftpUsers = $allFtpUsers + ->filter(fn($ftp) => $ftp['parent_domain_id'] == $domainId) + ->map(fn($ftp) => [ + 'ftp_user_id' => $ftp['ftp_user_id'], + 'username' => $ftp['username'], + 'dir' => $ftp['dir'], + ]) + ->values(); + + // Filtrage des utilisateurs Shell pour ce site + $shellUsers = $allShellUsers + ->filter(fn($shell) => $shell['parent_domain_id'] == $domainId) + ->map(fn($shell) => [ + 'shell_user_id' => $shell['shell_user_id'], + 'username' => $shell['username'], + 'shell' => $shell['shell'], + 'chroot' => $shell['chroot'], + ]) + ->values(); + + // Filtrage des zones DNS pour ce site + // Le champ 'origin' de la zone DNS correspond au domaine avec un point final + $dnsZones = $allDnsZones + ->filter(function ($zone) use ($domain, $aliases) { + // Normalisation : retirer le point final de l'origin pour comparer + $zoneOrigin = rtrim($zone['origin'], '.'); + + // Vérifier le domaine principal + if (strtolower($zoneOrigin) === strtolower($domain)) { + return true; + } + + // Vérifier les alias + foreach ($aliases as $alias) { + if (strtolower($zoneOrigin) === strtolower($alias)) { + return true; + } + } + + return false; + }) + ->map(fn($zone) => [ + 'id' => $zone['id'], + 'origin' => $zone['origin'], + 'ns' => $zone['ns'], + 'active' => $zone['active'], + 'dnssec_wanted' => $zone['dnssec_wanted'] ?? null, + 'dnssec_initialized' => $zone['dnssec_initialized'] ?? null, + ]) + ->values(); + + return [ + 'domain_id' => $domainId, + 'domain' => $domain, + 'document_root' => $site['document_root'], + 'active' => $site['active'], + 'aliases' => $aliases, + 'databases' => $databases, + 'ftp_users' => $ftpUsers, + 'shell_users' => $shellUsers, + 'dns_zones' => $dnsZones, + ]; + }); + + // Création/mise à jour d'un enregistrement par site + foreach ($sitesData as $siteData) { + IspconfigMember::updateOrCreate( + [ + 'member_id' => $member->id, + 'type' => IspconfigType::WEB, + 'ispconfig_service_user_id' => $siteData['domain_id'], + ], + [ + 'data' => $siteData, + ] + ); + } + + $progressBar->advance(); + } + }); + + $progressBar->finish(); + $this->info('Synchronisation WEB terminée'); + } + + /** + * Normalise une URL vers un domaine + */ + 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; + } +} diff --git a/app/Enums/IspconfigType.php b/app/Enums/IspconfigType.php new file mode 100644 index 0000000..92f2593 --- /dev/null +++ b/app/Enums/IspconfigType.php @@ -0,0 +1,19 @@ + 'Email', + self::WEB => 'Hébergement', + self::OTHER => 'Autre', + }; + } +} diff --git a/app/Filament/Resources/Members/Schemas/MemberForm.php b/app/Filament/Resources/Members/Schemas/MemberForm.php index 3e0f154..485fdb8 100644 --- a/app/Filament/Resources/Members/Schemas/MemberForm.php +++ b/app/Filament/Resources/Members/Schemas/MemberForm.php @@ -8,11 +8,11 @@ use Filament\Forms\Components\DatePicker; use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Toggle; -use Filament\Schemas\Components\Actions; use Filament\Schemas\Components\Grid; use Filament\Schemas\Components\Section; use Filament\Schemas\Schema; -use Filament\Tables\Columns\Layout\Split; +use Filament\Infolists\Components\TextEntry; + class MemberForm { @@ -91,6 +91,41 @@ class MemberForm ->label(Member::getAttributeLabel('country')), ]) ->columns(2), + + Section::make('Messagerie ISPConfig Retzien') + ->schema([ + TextEntry::make('isp_mail_email') + ->label('Adresse email') + ->state(fn (?Member $record) => + $record?->ispconfigMail()?->email ?? '—' + ), + + TextEntry::make('isp_mail_user_id') + ->label('ID utilisateur ISPConfig (mailuser_id)') + ->state(fn (?Member $record) => + $record?->ispconfigMail()?->ispconfig_service_user_id ?? '—' + ), + + TextEntry::make('isp_mail_quota') + ->label('Quota') + ->state(function (?Member $record) { + $quota = $record?->ispconfigMail()?->data['mailuser']['quota'] ?? null; + + return $quota + ? "{$quota} Mo" + : 'Non défini'; + }), + + TextEntry::make('isp_mail_domain') + ->label('Domaine') + ->state(fn (?Member $record) => + $record?->ispconfigMail()?->data['mailuser']['domain'] ?? 'retzien.fr' + ), + ]) + ->columns(2) + ->visible(fn (?Member $record) => + $record?->ispconfigMail() !== null + ), ]) ->columnSpan(3), Grid::make(1) diff --git a/app/Models/IspconfigMember.php b/app/Models/IspconfigMember.php new file mode 100644 index 0000000..3e7aa9b --- /dev/null +++ b/app/Models/IspconfigMember.php @@ -0,0 +1,31 @@ + IspconfigType::class, + 'data' => 'array', + ]; + + public function member(): BelongsTo + { + return $this->belongsTo(Member::class); + } +} diff --git a/app/Models/Member.php b/app/Models/Member.php index 3815b2c..9ed0c37 100644 --- a/app/Models/Member.php +++ b/app/Models/Member.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Enums\IspconfigType; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -122,4 +123,23 @@ class Member extends Model { return $this->hasMany(Membership::class); } + + public function ispconfigs(): HasMany + { + return $this->hasMany(IspconfigMember::class, 'member_id'); + } + + public function ispconfigMail(): ?IspconfigMember + { + return $this->ispconfigs() + ->where('type', IspconfigType::MAIL) + ->first(); + } + + public function ispconfigWeb(): ?IspconfigMember + { + return $this->ispconfigs() + ->where('type', IspconfigType::WEB) + ->first(); + } } diff --git a/app/Services/ISPConfig/ISPConfigWebService.php b/app/Services/ISPConfig/ISPConfigWebService.php index b363c91..568b1a5 100644 --- a/app/Services/ISPConfig/ISPConfigWebService.php +++ b/app/Services/ISPConfig/ISPConfigWebService.php @@ -2,6 +2,8 @@ namespace App\Services\ISPConfig; +use Illuminate\Support\Facades\Cache; + class ISPConfigWebService extends ISPConfigService { public function __construct() @@ -14,6 +16,251 @@ class ISPConfigWebService extends ISPConfigService */ public function getAllWebsites(): array { - return $this->call('sites_web_domain_get', [['primary_id' => -1]]); + return Cache::remember( + "ispconfig.web.websites.all", + config('services.ispconfig.cache_ttl'), + fn() => $this->call('sites_web_domain_get', ['primary_id' => -1]) + ); + } + + /** + * @throws \Exception + */ + public function getAllDatabases(): array + { + return Cache::remember( + "ispconfig.web.databases.all", + config('services.ispconfig.cache_ttl'), + fn() => $this->call('sites_database_get', ['primary_id' => -1]) + ); + } + + /** + * @throws \Exception + */ + public function getAllFtpUsers(): array + { + return Cache::remember( + "ispconfig.web.ftp.all", + config('services.ispconfig.cache_ttl'), + fn() => $this->call('sites_ftp_user_get', ['primary_id' => -1]) + ); + } + + /** + * @throws \Exception + */ + public function getAllShellUsers(): array + { + return Cache::remember( + "ispconfig.web.shell.all", + config('services.ispconfig.cache_ttl'), + fn() => $this->call('sites_shell_user_get', ['primary_id' => -1]) + ); + } + + /** + * @throws \Exception + */ + public function getAllDnsZones(): array + { + return Cache::remember( + "ispconfig.web.dns-zones.all", + config('services.ispconfig.cache_ttl'), + 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 + */ + public function getWebsiteAliases(int $domainId): array + { + return Cache::remember( + "ispconfig.web.aliases.{$domainId}", + config('services.ispconfig.cache_ttl', 3600), + function () use ($domainId) { + try { + $siteInfo = $this->call('sites_web_domain_get', ['domain_id' => $domainId]); + + if (empty($siteInfo)) { + return []; + } + + $site = $siteInfo; + + if (empty($site['alias'])) { + return []; + } + + $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 []; + } + } + ); + } + + /** + * 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 + */ + public function getWebsiteDatabases(int $sysGroupId): array + { + // Récupération de toutes les bases de données + $allDatabases = $this->getAllDatabases(); + + // Filtrage par sys_groupid + return collect($allDatabases) + ->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'] ?? '' + ]) + ->values() + ->toArray(); + } + + /** + * 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 + */ + public function getWebsiteFtpUsers(int $domainId): array + { + // Récupération de tous les utilisateurs FTP + $allFtpUsers = $this->getAllFtpUsers(); + + // Filtrage par parent_domain_id + return collect($allFtpUsers) + ->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'] + ]) + ->values() + ->toArray(); + } + + /** + * 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 + */ + public function getWebsiteShellUsers(int $domainId): array + { + // Récupération de tous les utilisateurs Shell (avec cache) + $allShellUsers = $this->getAllShellUsers(); + + // Filtrage par parent_domain_id + return collect($allShellUsers) + ->filter(fn($shell) => $shell['parent_domain_id'] == $domainId) + ->map(fn($shell) => [ + 'shell_user_id' => $shell['shell_user_id'], + 'username' => $shell['username'], + 'dir' => $shell['dir'], + 'shell' => $shell['shell'], + 'puser' => $shell['puser'], + 'pgroup' => $shell['pgroup'], + 'quota_size' => $shell['quota_size'], + 'active' => $shell['active'], + 'chroot' => $shell['chroot'], + 'ssh_rsa' => !empty($shell['ssh_rsa']) + ]) + ->values() + ->toArray(); + } + + /** + * Récupère toutes les informations complètes d'un site (alias, BDD, FTP, Shell) + * + * @param int $domainId + * @return array|null + * @throws \Exception + */ + public function getWebsiteCompleteInfo(int $domainId): ?array + { + return Cache::remember( + "ispconfig.web.complete.{$domainId}", + config('services.ispconfig.cache_ttl', 3600), + function () use ($domainId) { + $siteInfo = $this->call('sites_web_domain_get', ['domain_id' => $domainId]); + + if (empty($siteInfo)) { + return null; + } + + $site = $siteInfo; + + // Récupérer les alias + $aliases = []; + if (!empty($site['alias'])) { + $aliases = array_values(array_filter(array_map('trim', explode(',', $site['alias'])))); + } + + return [ + 'domain_id' => $site['domain_id'], + 'domain' => $site['domain'], + 'document_root' => $site['document_root'], + 'active' => $site['active'], + 'sys_groupid' => $site['sys_groupid'], + 'aliases' => $aliases, + 'databases' => $this->getWebsiteDatabases($domainId, $site['sys_groupid']), + 'ftp_users' => $this->getWebsiteFtpUsers($domainId), + 'shell_users' => $this->getWebsiteShellUsers($domainId) + ]; + } + ); + } + + /** + * Vide le cache pour un domaine spécifique + * + * @param int $domainId + * @return void + */ + public function clearDomainCache(int $domainId): void + { + Cache::forget("ispconfig.web.aliases.{$domainId}"); + Cache::forget("ispconfig.web.complete.{$domainId}"); + } + + /** + * 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"); } } diff --git a/database/migrations/2025_12_29_150540_add_ispconfig_ids_to_members_table.php b/database/migrations/2025_12_29_150540_add_ispconfig_ids_to_members_table.php deleted file mode 100644 index b93cfca..0000000 --- a/database/migrations/2025_12_29_150540_add_ispconfig_ids_to_members_table.php +++ /dev/null @@ -1,31 +0,0 @@ -string('ispconfig_mail_client_id')->after('dolibarr_id')->nullable(); - $table->string('ispconfig_web_client_id')->after('ispconfig_mail_client_id')->nullable(); - - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('members', function (Blueprint $table) { - $table->dropColumn('ispconfig_mail_client_id'); - $table->dropColumn('ispconfig_web_client_id'); - }); - } -}; diff --git a/database/migrations/2025_12_30_134044_create_ispconfigs_members_table.php b/database/migrations/2025_12_30_134044_create_ispconfigs_members_table.php new file mode 100644 index 0000000..3695fad --- /dev/null +++ b/database/migrations/2025_12_30_134044_create_ispconfigs_members_table.php @@ -0,0 +1,33 @@ +id(); + $table->foreignId('member_id')->constrained('members')->onDelete('NO ACTION'); + $table->string('ispconfig_client_id')->nullable(); + $table->string('ispconfig_service_user_id')->nullable(); + $table->string('email')->nullable(); + $table->enum('type', ['mail', 'web', 'other']); + $table->json('data')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('ispconfigs_members'); + } +}; diff --git a/routes/web.php b/routes/web.php index 4d8f7d9..348ff0e 100644 --- a/routes/web.php +++ b/routes/web.php @@ -25,7 +25,7 @@ Route::get('/test/sync-ispconfig', function () { abort(403); } - Artisan::call('sync:ispconfig-members'); + Artisan::call('sync:ispconfig-web-members'); return response()->json([ 'status' => 'ok',