diff --git a/app/Console/Commands/HandleExpiredMembersDolibarr.php b/app/Console/Commands/HandleExpiredMembersDolibarr.php index 5a7fdd5..b87d04e 100644 --- a/app/Console/Commands/HandleExpiredMembersDolibarr.php +++ b/app/Console/Commands/HandleExpiredMembersDolibarr.php @@ -13,14 +13,15 @@ use Symfony\Component\Console\Command\Command as CommandAlias; class HandleExpiredMembersDolibarr extends Command { protected $signature = 'members:cleanup-expired - {email? : Adresse email d\'un adhérent à traiter uniquement} - {--dry-run}'; + {email? : Adresse email d\'un adhérent à traiter uniquement} + {--dry-run}'; public function __construct( - protected DolibarrService $dolibarr, + protected DolibarrService $dolibarr, protected ISPConfigMailService $mailService, - protected NextcloudService $nextcloud - ) { + protected NextcloudService $nextcloud + ) + { parent::__construct(); } @@ -39,7 +40,7 @@ class HandleExpiredMembersDolibarr extends Command ); if ($emailFilter) { - $this->info("Mode utilisateur unique : {$emailFilter}"); + $this->warn("Mode utilisateur unique : {$emailFilter}"); } $this->info('Récupération des adhérents Dolibarr'); @@ -61,8 +62,7 @@ class HandleExpiredMembersDolibarr extends Command if ($emailFilter) { $expiredMembers = $expiredMembers->filter(function ($member) use ($emailFilter) { - $email = $this->extractRetzienEmail($member['email'] ?? null); - return $email === $emailFilter; + return $this->extractRetzienEmail($member['email'] ?? null) === $emailFilter; }); if ($expiredMembers->isEmpty()) { @@ -71,7 +71,6 @@ class HandleExpiredMembersDolibarr extends Command } } - $this->info("{$expiredMembers->count()} adhérent(s) expiré(s)"); foreach ($expiredMembers as $member) { @@ -90,11 +89,13 @@ class HandleExpiredMembersDolibarr extends Command ? 'DRY-RUN terminé – aucune action effectuée' : 'Traitement terminé' ); + return CommandAlias::SUCCESS; } /** * @throws ConnectionException + * @throws \Exception */ protected function processMember(array $member, bool $dryRun): void { @@ -106,7 +107,9 @@ class HandleExpiredMembersDolibarr extends Command if ($dryRun) { $this->info("[DRY-RUN] Résiliation Dolibarr"); } else { - $this->dolibarr->setMemberStatus($member['id'], '0'); + $this->dolibarr->updateMember($member['id'], [ + 'statut' => 0, + ]); } // Désactivation mail @@ -114,7 +117,7 @@ class HandleExpiredMembersDolibarr extends Command $this->disableMailAccount($email, $dryRun); } - // Désactivation Nextcloud + // Désactivation Nextcloud if ($email) { if ($dryRun) { $this->info("[DRY-RUN] Désactivation Nextcloud"); @@ -124,6 +127,9 @@ class HandleExpiredMembersDolibarr extends Command } } + /** + * @throws \Exception + */ protected function disableMailAccount(string $email, bool $dryRun): void { $details = $this->mailService->getMailUserDetails($email); @@ -134,18 +140,22 @@ class HandleExpiredMembersDolibarr extends Command } if ($dryRun) { - $this->info("[DRY-RUN] Mail désactivé"); + $this->info("[DRY-RUN] Mail désactivé ({$email})"); return; } - $this->mailService->updateMailUser($email, [ + $result = $this->mailService->updateMailUser($email, [ 'postfix' => 'n', 'disablesmtp' => 'y', 'disableimap' => 'y', 'disablepop3' => 'y', ]); - $this->info("Mail désactivé"); + if (!$result) { + throw new \RuntimeException("Échec désactivation mail ISPConfig pour {$email}"); + } + + $this->info("Mail désactivé : {$email}"); } protected function extractRetzienEmail(?string $emails): ?string @@ -155,8 +165,8 @@ class HandleExpiredMembersDolibarr extends Command } return collect(explode(';', $emails)) - ->map(fn (string $email): string => trim($email)) - ->filter(fn (string $email): bool => str_contains($email, '@retzien.fr')) + ->map(fn(string $email): string => trim($email)) + ->filter(fn(string $email): bool => str_contains($email, '@retzien.fr')) ->first(); } } diff --git a/app/Console/Commands/SyncNextcloudMembers.php b/app/Console/Commands/SyncNextcloudMembers.php new file mode 100644 index 0000000..cdae6fb --- /dev/null +++ b/app/Console/Commands/SyncNextcloudMembers.php @@ -0,0 +1,115 @@ +option('dry-run'); + $memberFilter = $this->option('member'); + + $this->info( + $dryRun + ? 'DRY-RUN activé' + : 'Synchronisation Nextcloud → Members' + ); + + /** index des membres par email */ + $members = Member::query() + ->where('email', 'like', '%@retzien.fr%') + ->when($memberFilter, fn ($q) => $q->where('id', $memberFilter)) + ->get() + ->filter(fn (Member $m) => !empty($m->retzien_email)) + ->keyBy(fn (Member $m) => strtolower($m->retzien_email)); + + + if ($members->isEmpty()) { + $this->warn('Aucun membre à synchroniser'); + return CommandAlias::SUCCESS; + } + + $this->info("{$members->count()} membres candidats"); + + /**Récupération des users Nextcloud */ + $userIds = $this->nextcloud->listUsers(); + + $this->info(count($userIds) . ' comptes Nextcloud trouvés'); + + $synced = 0; + + foreach ($userIds as $userId) { + try { + $details = $this->nextcloud->getUserDetails($userId); + + $email = strtolower($details['email'] ?? ''); + + if (!$email || !$members->has($email)) { + continue; + } + + $member = $members[$email]; + + $payload = [ + 'member_id' => $member->id, + 'nextcloud_user_id' => $userId, + 'data' => [ + 'email' => $email, + 'quota' => $details['quota'] ?? null, + 'groups' => $details['groups'] ?? [], + 'enabled' => !($details['enabled'] === false), + 'last_login' => $details['lastLogin'] ?? null, + 'raw' => $details, // utile pour debug + ], + ]; + + if ($dryRun) { + $this->line("[DRY-RUN] {$member->id} ← {$userId}"); + } else { + NextCloudMember::query()->updateOrCreate( + [ + 'member_id' => $member->id, + 'nextcloud_user_id' => $userId, + ], + $payload + ); + } + + $synced++; + + } catch (\Throwable $e) { + Log::error('Erreur sync Nextcloud', [ + 'user_id' => $userId, + 'error' => $e->getMessage(), + ]); + } + } + + $this->info("Synchronisation terminée ({$synced} comptes liés)"); + + return CommandAlias::SUCCESS; + } +} diff --git a/app/Models/Member.php b/app/Models/Member.php index 76de2c3..16c4140 100644 --- a/app/Models/Member.php +++ b/app/Models/Member.php @@ -109,6 +109,12 @@ class Member extends Model return "{$this->firstname} {$this->lastname}"; } + public function getRetzienEmailAttribute(): string + { + $emails = explode(';', $this->email); + return collect($emails)->filter(fn($email) => str_contains($email, '@retzien.fr'))->first(); + } + public function user(): BelongsTo { return $this->belongsTo(User::class); @@ -142,4 +148,9 @@ class Member extends Model ->where('type', IspconfigType::WEB) ->first(); } + + public function nextcloudAccounts(): HasMany + { + return $this->hasMany(NextCloudMember::class, 'member_id'); + } } diff --git a/app/Models/NextCloudMember.php b/app/Models/NextCloudMember.php new file mode 100644 index 0000000..33e8b4d --- /dev/null +++ b/app/Models/NextCloudMember.php @@ -0,0 +1,27 @@ + 'array', + ]; + + public function member(): BelongsTo + { + return $this->belongsTo(Member::class); + } +} diff --git a/app/Services/Dolibarr/DolibarrService.php b/app/Services/Dolibarr/DolibarrService.php index eec55f7..468d755 100644 --- a/app/Services/Dolibarr/DolibarrService.php +++ b/app/Services/Dolibarr/DolibarrService.php @@ -61,14 +61,22 @@ class DolibarrService return $response->json(); } - public function setMemberStatus(int|string $id, int|string $status): bool + /** + * Update a member with custom data + * + * @param int|string $id The Dolibarr member ID (rowid) + * @param array $data Array of attributes to update (e.g. ['email' => 'new@email.com', 'array_options' => ['options_custom' => 'val']]) + * @throws ConnectionException + */ + public function updateMember(int|string $id, array $data): bool { $response = $this->client()->put( $this->baseUrl . '/members/' . $id, - ['status' => $status] + $data ); return $response->successful(); } + } diff --git a/app/Services/ISPConfig/ISPConfigMailService.php b/app/Services/ISPConfig/ISPConfigMailService.php index df38aef..b22d16e 100644 --- a/app/Services/ISPConfig/ISPConfigMailService.php +++ b/app/Services/ISPConfig/ISPConfigMailService.php @@ -88,19 +88,44 @@ class ISPConfigMailService extends ISPConfigService ]; } + /** + * @throws \Exception + */ public function updateMailUser(string $email, array $changes): bool { - $allUsers = $this->getAllMailUsers(); - $user = collect($allUsers)->firstWhere('email', $email); + // On retrouve l'utilisateur + $user = collect($this->getAllMailUsers()) + ->firstWhere('email', $email); if (!$user) { return false; } - return $this->call('mail_user_update', [ - 'primary_id' => $user['mailuser_id'], - 'params' => $changes, + $mailuserId = (int) $user['mailuser_id']; + + // On récupère l'enregistrement COMPLET (OBLIGATOIRE) + $mailUserRecord = $this->call('mail_user_get', [ + $mailuserId ]); + + if (!is_array($mailUserRecord)) { + throw new \RuntimeException('mail_user_get did not return array'); + } + + // On applique les changements + foreach ($changes as $key => $value) { + $mailUserRecord[$key] = $value; + } + + // appel conforme EXACT à l’exemple ISPConfig + $result = $this->call('mail_user_update', [ + 0, // client_id (ADMIN) + $mailuserId, // primary_id + $mailUserRecord // FULL RECORD + ]); + + return (bool) $result; } + } diff --git a/app/Services/Nextcloud/NextcloudService.php b/app/Services/Nextcloud/NextcloudService.php index 3b5d010..8f369ca 100644 --- a/app/Services/Nextcloud/NextcloudService.php +++ b/app/Services/Nextcloud/NextcloudService.php @@ -4,6 +4,7 @@ namespace App\Services\Nextcloud; use Illuminate\Http\Client\ConnectionException; use Illuminate\Http\Client\PendingRequest; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; @@ -46,15 +47,24 @@ class NextcloudService /** * Trouve le userId Nextcloud à partir de l’email - * @throws ConnectionException */ protected function findUserIdByEmail(string $email): ?string { - $response = $this->http->get('/cloud/users'); + return Cache::remember( + 'nextcloud.user_id.' . md5($email), + now()->addDays(7), + function () use ($email) { + return $this->resolveUserIdByEmail($email); + } + ); + } - if (!$response->successful()) { - throw new \RuntimeException('Erreur récupération utilisateurs Nextcloud'); - } + /** + * @throws ConnectionException + */ + protected function resolveUserIdByEmail(string $email): ?string + { + $response = $this->http->get('/cloud/users'); $users = $response->json('ocs.data.users') ?? []; @@ -63,7 +73,7 @@ class NextcloudService if ( $details->successful() && - ($details->json('ocs.data.email') === $email) + $details->json('ocs.data.email') === $email ) { return $userId; } @@ -71,4 +81,26 @@ class NextcloudService return null; } + + /** + * @throws ConnectionException + */ + public function listUsers(): array + { + return $this->http + ->get('/cloud/users') + ->json('ocs.data.users') ?? []; + } + + /** + * @throws ConnectionException + */ + public function getUserDetails(string $userId): array + { + return $this->http + ->get("/cloud/users/{$userId}") + ->json('ocs.data') ?? []; + } + + } diff --git a/database/migrations/2026_01_19_105050_create_nextclouds_members_table.php b/database/migrations/2026_01_19_105050_create_nextclouds_members_table.php new file mode 100644 index 0000000..2e7345b --- /dev/null +++ b/database/migrations/2026_01_19_105050_create_nextclouds_members_table.php @@ -0,0 +1,30 @@ +id(); + $table->foreignId('member_id')->constrained('members')->onDelete('NO ACTION'); + $table->string('nextcloud_user_id')->nullable(); + $table->json('data')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('nextclouds_members'); + } +};