Comptabilite (Super Admin) : - ComptabiliteController avec 7 exports CSV/JSON compatibles SAGE (journal ventes, grand livre, FEC, balance agee, reglements, commissions Stripe 1.5%+0.25E, couts services) - Export PDF via ComptaPdf (FPDF) avec bloc legal pre-rempli, tableau pagine, champ signature DocuSeal - Signature electronique DocuSeal + callback + envoi email signe avec template dedie (compta_export_signed.html.twig) - Rapport financier public (RapportFinancierPdf) : recettes par service, depenses (Stripe, infra, prestataires), bilan excedent/deficit - Codes comptables clients EC-XXXX (plus de 411xxx) Prestataires (Super Admin) : - Entite Prestataire (raisonSociale, siret, email, phone, adresse) - Entite FacturePrestataire (numFacture, montantHt, montantTtc, year, month, isPaid, PDF via Vich) - CRUD complet avec recherche SIRET via proxy API data.gouv.fr - Commande cron app:reminder:factures-prestataire (5 du mois) - Factures prestataires integrees dans export couts services - Sidebar Super Admin : entree Prestataires + Comptabilite Stats (/admin/stats) : - Cout prestataire dynamique depuis FacturePrestataire - Fusion Infra + Prestataire en "Cout de fonctionnement" - Commission Stripe corrigee (1.5% + 0.25E par transaction) Divers : - DocuSealService::sendComptaForSignature() + getApi() - Customer::generateCodeComptable() format EC-XXXX-XXXXX - Protection double prefixe EC- a la creation client - Bouton regenerer PDF cache quand advert state=accepted - Modals sans script inline (data-modal-open/close dans app.js) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
324 lines
10 KiB
PHP
324 lines
10 KiB
PHP
<?php
|
|
|
|
namespace App\Service;
|
|
|
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
|
|
|
class KeycloakAdminService
|
|
{
|
|
private const PATH_USERS = '/users';
|
|
private const PATH_GROUPS = '/groups';
|
|
private const AUTH_BEARER = 'Bearer ';
|
|
|
|
/** Groupes requis pour le CRM E-Cosplay */
|
|
private const REQUIRED_GROUPS = [
|
|
'superadmin',
|
|
'super_admin_asso',
|
|
'gp_asso',
|
|
'gp_contest',
|
|
'gp_mail',
|
|
'gp_mailling',
|
|
'gp_member',
|
|
'gp_ndd',
|
|
'gp_sign',
|
|
'gp_ticket',
|
|
];
|
|
|
|
private ?string $accessToken = null;
|
|
private ?int $tokenExpiresAt = null;
|
|
|
|
public function __construct(
|
|
private HttpClientInterface $httpClient,
|
|
#[Autowire(env: 'OAUTH_KEYCLOAK_URL')] private string $keycloakUrl,
|
|
#[Autowire(env: 'OAUTH_KEYCLOAK_REALM')] private string $realm,
|
|
#[Autowire(env: 'KEYCLOAK_ADMIN_CLIENT_ID')] private string $clientId,
|
|
#[Autowire(env: 'KEYCLOAK_ADMIN_CLIENT_SECRET')] private string $clientSecret,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* Verifie que tous les groupes requis existent dans Keycloak, cree ceux qui manquent.
|
|
*
|
|
* @return list<string> liste des groupes crees
|
|
*/
|
|
public function ensureRequiredGroups(): array
|
|
{
|
|
$existingGroups = $this->listGroups();
|
|
$existingNames = array_map(fn (array $g) => $g['name'], $existingGroups);
|
|
$created = [];
|
|
|
|
foreach (self::REQUIRED_GROUPS as $groupName) {
|
|
if (!\in_array($groupName, $existingNames, true) && $this->createGroup($groupName)) {
|
|
$created[] = $groupName;
|
|
}
|
|
}
|
|
|
|
return $created;
|
|
}
|
|
|
|
/**
|
|
* Creer un groupe dans Keycloak.
|
|
*/
|
|
public function createGroup(string $groupName): bool
|
|
{
|
|
$token = $this->getToken();
|
|
$response = $this->httpClient->request('POST', $this->getAdminUrl(self::PATH_GROUPS), [
|
|
'headers' => ['Authorization' => self::AUTH_BEARER.$token],
|
|
'json' => ['name' => $groupName],
|
|
]);
|
|
|
|
return 201 === $response->getStatusCode();
|
|
}
|
|
|
|
/**
|
|
* Creer un utilisateur dans Keycloak.
|
|
*
|
|
* @return array{created: bool, keycloakId: string|null, tempPassword: string|null}
|
|
*/
|
|
public function createUser(string $email, string $firstName, string $lastName): array
|
|
{
|
|
$token = $this->getToken();
|
|
$tempPassword = bin2hex(random_bytes(8));
|
|
|
|
// Creer l'utilisateur
|
|
$response = $this->httpClient->request('POST', $this->getAdminUrl(self::PATH_USERS), [
|
|
'headers' => ['Authorization' => self::AUTH_BEARER.$token],
|
|
'json' => [
|
|
'username' => $email,
|
|
'email' => $email,
|
|
'firstName' => $firstName,
|
|
'lastName' => $lastName,
|
|
'enabled' => true,
|
|
'emailVerified' => true,
|
|
'requiredActions' => ['UPDATE_PASSWORD', 'CONFIGURE_TOTP'],
|
|
'credentials' => [
|
|
[
|
|
'type' => 'password',
|
|
'value' => $tempPassword,
|
|
'temporary' => true,
|
|
],
|
|
],
|
|
],
|
|
]);
|
|
|
|
if (201 !== $response->getStatusCode()) {
|
|
return ['created' => false, 'keycloakId' => null, 'tempPassword' => null];
|
|
}
|
|
|
|
// Recuperer l'ID du user cree
|
|
$locationHeader = $response->getHeaders(false)['location'][0] ?? '';
|
|
$keycloakId = basename($locationHeader);
|
|
|
|
if ('' === $keycloakId) {
|
|
$keycloakId = $this->getUserIdByEmail($email);
|
|
}
|
|
|
|
return [
|
|
'created' => true,
|
|
'keycloakId' => $keycloakId,
|
|
'tempPassword' => $tempPassword,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Ajouter un utilisateur a un groupe Keycloak.
|
|
*/
|
|
public function addUserToGroup(string $keycloakId, string $groupName): bool
|
|
{
|
|
$groupId = $this->getGroupIdByName($groupName);
|
|
|
|
if (null === $groupId) {
|
|
return false;
|
|
}
|
|
|
|
$token = $this->getToken();
|
|
$response = $this->httpClient->request('PUT', $this->getAdminUrl(self::PATH_USERS.'/'.$keycloakId.'/groups/'.$groupId), [
|
|
'headers' => ['Authorization' => self::AUTH_BEARER.$token],
|
|
]);
|
|
|
|
return 204 === $response->getStatusCode();
|
|
}
|
|
|
|
/**
|
|
* Supprimer un utilisateur de Keycloak.
|
|
*/
|
|
public function deleteUser(string $keycloakId): bool
|
|
{
|
|
$token = $this->getToken();
|
|
$response = $this->httpClient->request('DELETE', $this->getAdminUrl(self::PATH_USERS.'/'.$keycloakId), [
|
|
'headers' => ['Authorization' => self::AUTH_BEARER.$token],
|
|
]);
|
|
|
|
return 204 === $response->getStatusCode();
|
|
}
|
|
|
|
/**
|
|
* Mettre a jour les infos d'un utilisateur dans Keycloak.
|
|
*/
|
|
public function updateUser(string $keycloakId, string $firstName, string $lastName, string $email): bool
|
|
{
|
|
$token = $this->getToken();
|
|
$response = $this->httpClient->request('PUT', $this->getAdminUrl(self::PATH_USERS.'/'.$keycloakId), [
|
|
'headers' => ['Authorization' => self::AUTH_BEARER.$token],
|
|
'json' => [
|
|
'firstName' => $firstName,
|
|
'lastName' => $lastName,
|
|
'email' => $email,
|
|
'username' => $email,
|
|
],
|
|
]);
|
|
|
|
return 204 === $response->getStatusCode();
|
|
}
|
|
|
|
/**
|
|
* Reset le mot de passe d'un utilisateur dans Keycloak.
|
|
*/
|
|
public function resetPassword(string $keycloakId, string $newPassword): bool
|
|
{
|
|
$token = $this->getToken();
|
|
$response = $this->httpClient->request('PUT', $this->getAdminUrl(self::PATH_USERS.'/'.$keycloakId.'/reset-password'), [
|
|
'headers' => ['Authorization' => self::AUTH_BEARER.$token],
|
|
'json' => [
|
|
'type' => 'password',
|
|
'value' => $newPassword,
|
|
'temporary' => false,
|
|
],
|
|
]);
|
|
|
|
return 204 === $response->getStatusCode();
|
|
}
|
|
|
|
/**
|
|
* Envoyer un email de reset password a l'utilisateur.
|
|
*/
|
|
public function sendResetPasswordEmail(string $keycloakId): bool
|
|
{
|
|
$token = $this->getToken();
|
|
$response = $this->httpClient->request('PUT', $this->getAdminUrl(self::PATH_USERS.'/'.$keycloakId.'/execute-actions-email'), [
|
|
'headers' => ['Authorization' => self::AUTH_BEARER.$token],
|
|
'json' => ['UPDATE_PASSWORD'],
|
|
]);
|
|
|
|
return 200 === $response->getStatusCode() || 204 === $response->getStatusCode();
|
|
}
|
|
|
|
/**
|
|
* Recuperer les groupes d'un utilisateur.
|
|
*
|
|
* @return list<string>
|
|
*/
|
|
public function getUserGroups(string $keycloakId): array
|
|
{
|
|
$token = $this->getToken();
|
|
$response = $this->httpClient->request('GET', $this->getAdminUrl(self::PATH_USERS.'/'.$keycloakId.'/groups'), [
|
|
'headers' => ['Authorization' => self::AUTH_BEARER.$token],
|
|
]);
|
|
|
|
$groups = $response->toArray(false);
|
|
|
|
return array_map(fn (array $g) => $g['name'], $groups);
|
|
}
|
|
|
|
/**
|
|
* Lister tous les utilisateurs du realm.
|
|
*
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
public function listUsers(int $max = 100): array
|
|
{
|
|
$token = $this->getToken();
|
|
$response = $this->httpClient->request('GET', $this->getAdminUrl(self::PATH_USERS), [
|
|
'headers' => ['Authorization' => self::AUTH_BEARER.$token],
|
|
'query' => ['max' => $max],
|
|
]);
|
|
|
|
return $response->toArray(false);
|
|
}
|
|
|
|
/**
|
|
* Recuperer l'ID d'un utilisateur par email.
|
|
*/
|
|
public function getUserIdByEmail(string $email): ?string
|
|
{
|
|
$token = $this->getToken();
|
|
$response = $this->httpClient->request('GET', $this->getAdminUrl(self::PATH_USERS), [
|
|
'headers' => ['Authorization' => self::AUTH_BEARER.$token],
|
|
'query' => ['email' => $email, 'exact' => 'true'],
|
|
]);
|
|
|
|
$users = $response->toArray(false);
|
|
|
|
return $users[0]['id'] ?? null;
|
|
}
|
|
|
|
/**
|
|
* Lister tous les groupes du realm.
|
|
*
|
|
* @return list<array{id: string, name: string}>
|
|
*/
|
|
public function listGroups(): array
|
|
{
|
|
$token = $this->getToken();
|
|
$response = $this->httpClient->request('GET', $this->getAdminUrl(self::PATH_GROUPS), [
|
|
'headers' => ['Authorization' => self::AUTH_BEARER.$token],
|
|
]);
|
|
|
|
return $response->toArray(false);
|
|
}
|
|
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
public static function getRequiredGroups(): array
|
|
{
|
|
return self::REQUIRED_GROUPS;
|
|
}
|
|
|
|
private function getGroupIdByName(string $groupName): ?string
|
|
{
|
|
$token = $this->getToken();
|
|
$response = $this->httpClient->request('GET', $this->getAdminUrl(self::PATH_GROUPS), [
|
|
'headers' => ['Authorization' => self::AUTH_BEARER.$token],
|
|
'query' => ['search' => $groupName, 'exact' => 'true'],
|
|
]);
|
|
|
|
$groups = $response->toArray(false);
|
|
|
|
foreach ($groups as $group) {
|
|
if ($group['name'] === $groupName) {
|
|
return $group['id'];
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function getToken(): string
|
|
{
|
|
if (null !== $this->accessToken && null !== $this->tokenExpiresAt && time() < $this->tokenExpiresAt) {
|
|
return $this->accessToken;
|
|
}
|
|
|
|
$response = $this->httpClient->request('POST', $this->keycloakUrl.'/realms/'.$this->realm.'/protocol/openid-connect/token', [
|
|
'body' => [
|
|
'client_id' => $this->clientId,
|
|
'client_secret' => $this->clientSecret,
|
|
'grant_type' => 'client_credentials',
|
|
],
|
|
]);
|
|
|
|
$data = $response->toArray();
|
|
$this->accessToken = $data['access_token'];
|
|
$this->tokenExpiresAt = time() + ($data['expires_in'] ?? 300) - 30;
|
|
|
|
return $this->accessToken;
|
|
}
|
|
|
|
private function getAdminUrl(string $path): string
|
|
{
|
|
return $this->keycloakUrl.'/admin/realms/'.$this->realm.$path;
|
|
}
|
|
}
|