2026-04-01 15:42:52 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Service;
|
|
|
|
|
|
|
|
|
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
|
|
|
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
|
|
|
|
|
|
|
|
|
class KeycloakAdminService
|
|
|
|
|
{
|
2026-04-01 17:44:57 +02:00
|
|
|
private const PATH_USERS = '/users';
|
2026-04-03 11:14:47 +02:00
|
|
|
private const PATH_GROUPS = '/groups';
|
2026-04-01 17:44:57 +02:00
|
|
|
private const AUTH_BEARER = 'Bearer ';
|
|
|
|
|
|
feat: comptabilite + prestataires + rapport financier + stats dynamiques
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>
2026-04-07 23:39:31 +02:00
|
|
|
/** Groupes requis pour le CRM E-Cosplay */
|
feat: complete glassmorphism redesign across all templates + Keycloak groups auto-provisioning
Templates updated to glassmorphism (40+ files):
- templates/admin/clients/ (create, index): glass cards, input-glass inputs,
btn-gold buttons, glass table headers, semi-transparent badges
- templates/admin/dashboard.html.twig: glass KPI cards
- templates/admin/profil/index.html.twig: glass form panels
- templates/admin/revendeurs/ (create, edit, index): glass cards and tables
- templates/admin/services/index.html.twig: glass service cards
- templates/admin/status/ (index, manage): glass panels
- templates/admin/sync/index.html.twig: glass panels
- templates/admin/facturation/index.html.twig: glass tables
- templates/admin/membres.html.twig: glass form, checkboxes with esy-* group
values (esy-web, esy-mail, esy-mailer, esy-analytics, esy-monitor,
esy-defender, esy-translate, esy-signature, esy-creator, esy-aide,
esy-meet, esy-tchat, esy-ndd), Keycloak groups column in table,
available groups section
- templates/admin/stats/index.html.twig: glass KPI cards, glass-gold CA TTC,
factures emises/payees/impayees cards, services renamed to Esy-*,
rounded progress bars, bg-gray-200 track backgrounds
- templates/security/ (2fa_email, 2fa_google, forgot_password, set_password,
set_password_expired): glass headers, glass-heavy cards, input-glass
- templates/legal/ (cgu, cgv, cookie, conformite, hebergement,
mention_legal, rgpd, tarif): removed thick borders, font-black to
font-bold, text-3xl to text-2xl headings
- templates/attestation/ (verify, not_found): glass panels
- templates/espace_client/index.html.twig: glass panels
- templates/espace_prestataire/index.html.twig: glass panels
- templates/external_redirect.html.twig: glass card
- templates/status/index.html.twig: glass panels
- templates/email/base.html.twig: gradient gold header, rounded-16px
container, semi-transparent bg, soft shadow, footer address
- templates/emails/*.html.twig (9 files): removed 4px borders,
font-weight 900 to 700
- templates/pdf/*.html.twig (4 files): rounded borders, gradient header,
lighter borders
Keycloak auto-provisioning:
- src/Service/KeycloakAdminService.php: added REQUIRED_GROUPS constant
(15 groups: siteconseil_admin, siteconseil_member, esy-web, esy-mail,
esy-mailer, esy-analytics, esy-monitor, esy-defender, esy-translate,
esy-signature, esy-creator, esy-aide, esy-meet, esy-tchat, esy-ndd),
ensureRequiredGroups() method that checks existing groups and creates
missing ones, createGroup() method, getRequiredGroups() static accessor
- src/Controller/Admin/MembresController.php: calls ensureRequiredGroups()
on page load, shows flash for each auto-created group, fetches user
groups per member, passes availableGroups to template
Stats controller updated:
- src/Controller/Admin/StatsController.php: services renamed to Esy-*
(13 services), added factures_emises/payees/impayees KPI data
OAuth fix:
- src/Security/KeycloakAuthenticator.php: removed dd() debug calls,
restored flash message on auth failure with error detail
Config:
- .env: KEYCLOAK_ADMIN_CLIENT_ID=crm_siteconseil_admin, secret updated
- .env.local: same updates
- ansible/env.local.j2: KEYCLOAK_ADMIN_CLIENT_ID=crm_siteconseil_admin
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 19:34:35 +02:00
|
|
|
private const REQUIRED_GROUPS = [
|
feat: comptabilite + prestataires + rapport financier + stats dynamiques
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>
2026-04-07 23:39:31 +02:00
|
|
|
'superadmin',
|
|
|
|
|
'super_admin_asso',
|
|
|
|
|
'gp_asso',
|
|
|
|
|
'gp_contest',
|
|
|
|
|
'gp_mail',
|
|
|
|
|
'gp_mailling',
|
|
|
|
|
'gp_member',
|
|
|
|
|
'gp_ndd',
|
|
|
|
|
'gp_sign',
|
|
|
|
|
'gp_ticket',
|
feat: complete glassmorphism redesign across all templates + Keycloak groups auto-provisioning
Templates updated to glassmorphism (40+ files):
- templates/admin/clients/ (create, index): glass cards, input-glass inputs,
btn-gold buttons, glass table headers, semi-transparent badges
- templates/admin/dashboard.html.twig: glass KPI cards
- templates/admin/profil/index.html.twig: glass form panels
- templates/admin/revendeurs/ (create, edit, index): glass cards and tables
- templates/admin/services/index.html.twig: glass service cards
- templates/admin/status/ (index, manage): glass panels
- templates/admin/sync/index.html.twig: glass panels
- templates/admin/facturation/index.html.twig: glass tables
- templates/admin/membres.html.twig: glass form, checkboxes with esy-* group
values (esy-web, esy-mail, esy-mailer, esy-analytics, esy-monitor,
esy-defender, esy-translate, esy-signature, esy-creator, esy-aide,
esy-meet, esy-tchat, esy-ndd), Keycloak groups column in table,
available groups section
- templates/admin/stats/index.html.twig: glass KPI cards, glass-gold CA TTC,
factures emises/payees/impayees cards, services renamed to Esy-*,
rounded progress bars, bg-gray-200 track backgrounds
- templates/security/ (2fa_email, 2fa_google, forgot_password, set_password,
set_password_expired): glass headers, glass-heavy cards, input-glass
- templates/legal/ (cgu, cgv, cookie, conformite, hebergement,
mention_legal, rgpd, tarif): removed thick borders, font-black to
font-bold, text-3xl to text-2xl headings
- templates/attestation/ (verify, not_found): glass panels
- templates/espace_client/index.html.twig: glass panels
- templates/espace_prestataire/index.html.twig: glass panels
- templates/external_redirect.html.twig: glass card
- templates/status/index.html.twig: glass panels
- templates/email/base.html.twig: gradient gold header, rounded-16px
container, semi-transparent bg, soft shadow, footer address
- templates/emails/*.html.twig (9 files): removed 4px borders,
font-weight 900 to 700
- templates/pdf/*.html.twig (4 files): rounded borders, gradient header,
lighter borders
Keycloak auto-provisioning:
- src/Service/KeycloakAdminService.php: added REQUIRED_GROUPS constant
(15 groups: siteconseil_admin, siteconseil_member, esy-web, esy-mail,
esy-mailer, esy-analytics, esy-monitor, esy-defender, esy-translate,
esy-signature, esy-creator, esy-aide, esy-meet, esy-tchat, esy-ndd),
ensureRequiredGroups() method that checks existing groups and creates
missing ones, createGroup() method, getRequiredGroups() static accessor
- src/Controller/Admin/MembresController.php: calls ensureRequiredGroups()
on page load, shows flash for each auto-created group, fetches user
groups per member, passes availableGroups to template
Stats controller updated:
- src/Controller/Admin/StatsController.php: services renamed to Esy-*
(13 services), added factures_emises/payees/impayees KPI data
OAuth fix:
- src/Security/KeycloakAuthenticator.php: removed dd() debug calls,
restored flash message on auth failure with error detail
Config:
- .env: KEYCLOAK_ADMIN_CLIENT_ID=crm_siteconseil_admin, secret updated
- .env.local: same updates
- ansible/env.local.j2: KEYCLOAK_ADMIN_CLIENT_ID=crm_siteconseil_admin
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 19:34:35 +02:00
|
|
|
];
|
|
|
|
|
|
2026-04-01 15:42:52 +02:00
|
|
|
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,
|
|
|
|
|
) {
|
|
|
|
|
}
|
|
|
|
|
|
feat: complete glassmorphism redesign across all templates + Keycloak groups auto-provisioning
Templates updated to glassmorphism (40+ files):
- templates/admin/clients/ (create, index): glass cards, input-glass inputs,
btn-gold buttons, glass table headers, semi-transparent badges
- templates/admin/dashboard.html.twig: glass KPI cards
- templates/admin/profil/index.html.twig: glass form panels
- templates/admin/revendeurs/ (create, edit, index): glass cards and tables
- templates/admin/services/index.html.twig: glass service cards
- templates/admin/status/ (index, manage): glass panels
- templates/admin/sync/index.html.twig: glass panels
- templates/admin/facturation/index.html.twig: glass tables
- templates/admin/membres.html.twig: glass form, checkboxes with esy-* group
values (esy-web, esy-mail, esy-mailer, esy-analytics, esy-monitor,
esy-defender, esy-translate, esy-signature, esy-creator, esy-aide,
esy-meet, esy-tchat, esy-ndd), Keycloak groups column in table,
available groups section
- templates/admin/stats/index.html.twig: glass KPI cards, glass-gold CA TTC,
factures emises/payees/impayees cards, services renamed to Esy-*,
rounded progress bars, bg-gray-200 track backgrounds
- templates/security/ (2fa_email, 2fa_google, forgot_password, set_password,
set_password_expired): glass headers, glass-heavy cards, input-glass
- templates/legal/ (cgu, cgv, cookie, conformite, hebergement,
mention_legal, rgpd, tarif): removed thick borders, font-black to
font-bold, text-3xl to text-2xl headings
- templates/attestation/ (verify, not_found): glass panels
- templates/espace_client/index.html.twig: glass panels
- templates/espace_prestataire/index.html.twig: glass panels
- templates/external_redirect.html.twig: glass card
- templates/status/index.html.twig: glass panels
- templates/email/base.html.twig: gradient gold header, rounded-16px
container, semi-transparent bg, soft shadow, footer address
- templates/emails/*.html.twig (9 files): removed 4px borders,
font-weight 900 to 700
- templates/pdf/*.html.twig (4 files): rounded borders, gradient header,
lighter borders
Keycloak auto-provisioning:
- src/Service/KeycloakAdminService.php: added REQUIRED_GROUPS constant
(15 groups: siteconseil_admin, siteconseil_member, esy-web, esy-mail,
esy-mailer, esy-analytics, esy-monitor, esy-defender, esy-translate,
esy-signature, esy-creator, esy-aide, esy-meet, esy-tchat, esy-ndd),
ensureRequiredGroups() method that checks existing groups and creates
missing ones, createGroup() method, getRequiredGroups() static accessor
- src/Controller/Admin/MembresController.php: calls ensureRequiredGroups()
on page load, shows flash for each auto-created group, fetches user
groups per member, passes availableGroups to template
Stats controller updated:
- src/Controller/Admin/StatsController.php: services renamed to Esy-*
(13 services), added factures_emises/payees/impayees KPI data
OAuth fix:
- src/Security/KeycloakAuthenticator.php: removed dd() debug calls,
restored flash message on auth failure with error detail
Config:
- .env: KEYCLOAK_ADMIN_CLIENT_ID=crm_siteconseil_admin, secret updated
- .env.local: same updates
- ansible/env.local.j2: KEYCLOAK_ADMIN_CLIENT_ID=crm_siteconseil_admin
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 19:34:35 +02:00
|
|
|
/**
|
|
|
|
|
* 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) {
|
2026-04-03 11:14:47 +02:00
|
|
|
if (!\in_array($groupName, $existingNames, true) && $this->createGroup($groupName)) {
|
|
|
|
|
$created[] = $groupName;
|
feat: complete glassmorphism redesign across all templates + Keycloak groups auto-provisioning
Templates updated to glassmorphism (40+ files):
- templates/admin/clients/ (create, index): glass cards, input-glass inputs,
btn-gold buttons, glass table headers, semi-transparent badges
- templates/admin/dashboard.html.twig: glass KPI cards
- templates/admin/profil/index.html.twig: glass form panels
- templates/admin/revendeurs/ (create, edit, index): glass cards and tables
- templates/admin/services/index.html.twig: glass service cards
- templates/admin/status/ (index, manage): glass panels
- templates/admin/sync/index.html.twig: glass panels
- templates/admin/facturation/index.html.twig: glass tables
- templates/admin/membres.html.twig: glass form, checkboxes with esy-* group
values (esy-web, esy-mail, esy-mailer, esy-analytics, esy-monitor,
esy-defender, esy-translate, esy-signature, esy-creator, esy-aide,
esy-meet, esy-tchat, esy-ndd), Keycloak groups column in table,
available groups section
- templates/admin/stats/index.html.twig: glass KPI cards, glass-gold CA TTC,
factures emises/payees/impayees cards, services renamed to Esy-*,
rounded progress bars, bg-gray-200 track backgrounds
- templates/security/ (2fa_email, 2fa_google, forgot_password, set_password,
set_password_expired): glass headers, glass-heavy cards, input-glass
- templates/legal/ (cgu, cgv, cookie, conformite, hebergement,
mention_legal, rgpd, tarif): removed thick borders, font-black to
font-bold, text-3xl to text-2xl headings
- templates/attestation/ (verify, not_found): glass panels
- templates/espace_client/index.html.twig: glass panels
- templates/espace_prestataire/index.html.twig: glass panels
- templates/external_redirect.html.twig: glass card
- templates/status/index.html.twig: glass panels
- templates/email/base.html.twig: gradient gold header, rounded-16px
container, semi-transparent bg, soft shadow, footer address
- templates/emails/*.html.twig (9 files): removed 4px borders,
font-weight 900 to 700
- templates/pdf/*.html.twig (4 files): rounded borders, gradient header,
lighter borders
Keycloak auto-provisioning:
- src/Service/KeycloakAdminService.php: added REQUIRED_GROUPS constant
(15 groups: siteconseil_admin, siteconseil_member, esy-web, esy-mail,
esy-mailer, esy-analytics, esy-monitor, esy-defender, esy-translate,
esy-signature, esy-creator, esy-aide, esy-meet, esy-tchat, esy-ndd),
ensureRequiredGroups() method that checks existing groups and creates
missing ones, createGroup() method, getRequiredGroups() static accessor
- src/Controller/Admin/MembresController.php: calls ensureRequiredGroups()
on page load, shows flash for each auto-created group, fetches user
groups per member, passes availableGroups to template
Stats controller updated:
- src/Controller/Admin/StatsController.php: services renamed to Esy-*
(13 services), added factures_emises/payees/impayees KPI data
OAuth fix:
- src/Security/KeycloakAuthenticator.php: removed dd() debug calls,
restored flash message on auth failure with error detail
Config:
- .env: KEYCLOAK_ADMIN_CLIENT_ID=crm_siteconseil_admin, secret updated
- .env.local: same updates
- ansible/env.local.j2: KEYCLOAK_ADMIN_CLIENT_ID=crm_siteconseil_admin
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 19:34:35 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $created;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Creer un groupe dans Keycloak.
|
|
|
|
|
*/
|
|
|
|
|
public function createGroup(string $groupName): bool
|
|
|
|
|
{
|
|
|
|
|
$token = $this->getToken();
|
2026-04-03 11:14:47 +02:00
|
|
|
$response = $this->httpClient->request('POST', $this->getAdminUrl(self::PATH_GROUPS), [
|
feat: complete glassmorphism redesign across all templates + Keycloak groups auto-provisioning
Templates updated to glassmorphism (40+ files):
- templates/admin/clients/ (create, index): glass cards, input-glass inputs,
btn-gold buttons, glass table headers, semi-transparent badges
- templates/admin/dashboard.html.twig: glass KPI cards
- templates/admin/profil/index.html.twig: glass form panels
- templates/admin/revendeurs/ (create, edit, index): glass cards and tables
- templates/admin/services/index.html.twig: glass service cards
- templates/admin/status/ (index, manage): glass panels
- templates/admin/sync/index.html.twig: glass panels
- templates/admin/facturation/index.html.twig: glass tables
- templates/admin/membres.html.twig: glass form, checkboxes with esy-* group
values (esy-web, esy-mail, esy-mailer, esy-analytics, esy-monitor,
esy-defender, esy-translate, esy-signature, esy-creator, esy-aide,
esy-meet, esy-tchat, esy-ndd), Keycloak groups column in table,
available groups section
- templates/admin/stats/index.html.twig: glass KPI cards, glass-gold CA TTC,
factures emises/payees/impayees cards, services renamed to Esy-*,
rounded progress bars, bg-gray-200 track backgrounds
- templates/security/ (2fa_email, 2fa_google, forgot_password, set_password,
set_password_expired): glass headers, glass-heavy cards, input-glass
- templates/legal/ (cgu, cgv, cookie, conformite, hebergement,
mention_legal, rgpd, tarif): removed thick borders, font-black to
font-bold, text-3xl to text-2xl headings
- templates/attestation/ (verify, not_found): glass panels
- templates/espace_client/index.html.twig: glass panels
- templates/espace_prestataire/index.html.twig: glass panels
- templates/external_redirect.html.twig: glass card
- templates/status/index.html.twig: glass panels
- templates/email/base.html.twig: gradient gold header, rounded-16px
container, semi-transparent bg, soft shadow, footer address
- templates/emails/*.html.twig (9 files): removed 4px borders,
font-weight 900 to 700
- templates/pdf/*.html.twig (4 files): rounded borders, gradient header,
lighter borders
Keycloak auto-provisioning:
- src/Service/KeycloakAdminService.php: added REQUIRED_GROUPS constant
(15 groups: siteconseil_admin, siteconseil_member, esy-web, esy-mail,
esy-mailer, esy-analytics, esy-monitor, esy-defender, esy-translate,
esy-signature, esy-creator, esy-aide, esy-meet, esy-tchat, esy-ndd),
ensureRequiredGroups() method that checks existing groups and creates
missing ones, createGroup() method, getRequiredGroups() static accessor
- src/Controller/Admin/MembresController.php: calls ensureRequiredGroups()
on page load, shows flash for each auto-created group, fetches user
groups per member, passes availableGroups to template
Stats controller updated:
- src/Controller/Admin/StatsController.php: services renamed to Esy-*
(13 services), added factures_emises/payees/impayees KPI data
OAuth fix:
- src/Security/KeycloakAuthenticator.php: removed dd() debug calls,
restored flash message on auth failure with error detail
Config:
- .env: KEYCLOAK_ADMIN_CLIENT_ID=crm_siteconseil_admin, secret updated
- .env.local: same updates
- ansible/env.local.j2: KEYCLOAK_ADMIN_CLIENT_ID=crm_siteconseil_admin
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 19:34:35 +02:00
|
|
|
'headers' => ['Authorization' => self::AUTH_BEARER.$token],
|
|
|
|
|
'json' => ['name' => $groupName],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return 201 === $response->getStatusCode();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 15:42:52 +02:00
|
|
|
/**
|
|
|
|
|
* 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
|
2026-04-01 17:44:57 +02:00
|
|
|
$response = $this->httpClient->request('POST', $this->getAdminUrl(self::PATH_USERS), [
|
|
|
|
|
'headers' => ['Authorization' => self::AUTH_BEARER.$token],
|
2026-04-01 15:42:52 +02:00
|
|
|
'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();
|
2026-04-01 17:44:57 +02:00
|
|
|
$response = $this->httpClient->request('PUT', $this->getAdminUrl(self::PATH_USERS.'/'.$keycloakId.'/groups/'.$groupId), [
|
|
|
|
|
'headers' => ['Authorization' => self::AUTH_BEARER.$token],
|
2026-04-01 15:42:52 +02:00
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return 204 === $response->getStatusCode();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Supprimer un utilisateur de Keycloak.
|
|
|
|
|
*/
|
|
|
|
|
public function deleteUser(string $keycloakId): bool
|
|
|
|
|
{
|
|
|
|
|
$token = $this->getToken();
|
2026-04-01 17:44:57 +02:00
|
|
|
$response = $this->httpClient->request('DELETE', $this->getAdminUrl(self::PATH_USERS.'/'.$keycloakId), [
|
|
|
|
|
'headers' => ['Authorization' => self::AUTH_BEARER.$token],
|
2026-04-01 15:42:52 +02:00
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
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();
|
2026-04-01 17:44:57 +02:00
|
|
|
$response = $this->httpClient->request('PUT', $this->getAdminUrl(self::PATH_USERS.'/'.$keycloakId), [
|
|
|
|
|
'headers' => ['Authorization' => self::AUTH_BEARER.$token],
|
2026-04-01 15:42:52 +02:00
|
|
|
'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();
|
2026-04-01 17:44:57 +02:00
|
|
|
$response = $this->httpClient->request('PUT', $this->getAdminUrl(self::PATH_USERS.'/'.$keycloakId.'/reset-password'), [
|
|
|
|
|
'headers' => ['Authorization' => self::AUTH_BEARER.$token],
|
2026-04-01 15:42:52 +02:00
|
|
|
'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();
|
2026-04-01 17:44:57 +02:00
|
|
|
$response = $this->httpClient->request('PUT', $this->getAdminUrl(self::PATH_USERS.'/'.$keycloakId.'/execute-actions-email'), [
|
|
|
|
|
'headers' => ['Authorization' => self::AUTH_BEARER.$token],
|
2026-04-01 15:42:52 +02:00
|
|
|
'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();
|
2026-04-01 17:44:57 +02:00
|
|
|
$response = $this->httpClient->request('GET', $this->getAdminUrl(self::PATH_USERS.'/'.$keycloakId.'/groups'), [
|
|
|
|
|
'headers' => ['Authorization' => self::AUTH_BEARER.$token],
|
2026-04-01 15:42:52 +02:00
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$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();
|
2026-04-01 17:44:57 +02:00
|
|
|
$response = $this->httpClient->request('GET', $this->getAdminUrl(self::PATH_USERS), [
|
|
|
|
|
'headers' => ['Authorization' => self::AUTH_BEARER.$token],
|
2026-04-01 15:42:52 +02:00
|
|
|
'query' => ['max' => $max],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return $response->toArray(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Recuperer l'ID d'un utilisateur par email.
|
|
|
|
|
*/
|
|
|
|
|
public function getUserIdByEmail(string $email): ?string
|
|
|
|
|
{
|
|
|
|
|
$token = $this->getToken();
|
2026-04-01 17:44:57 +02:00
|
|
|
$response = $this->httpClient->request('GET', $this->getAdminUrl(self::PATH_USERS), [
|
|
|
|
|
'headers' => ['Authorization' => self::AUTH_BEARER.$token],
|
2026-04-01 15:42:52 +02:00
|
|
|
'query' => ['email' => $email, 'exact' => 'true'],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$users = $response->toArray(false);
|
|
|
|
|
|
|
|
|
|
return $users[0]['id'] ?? null;
|
|
|
|
|
}
|
|
|
|
|
|
feat: complete glassmorphism redesign across all templates + Keycloak groups auto-provisioning
Templates updated to glassmorphism (40+ files):
- templates/admin/clients/ (create, index): glass cards, input-glass inputs,
btn-gold buttons, glass table headers, semi-transparent badges
- templates/admin/dashboard.html.twig: glass KPI cards
- templates/admin/profil/index.html.twig: glass form panels
- templates/admin/revendeurs/ (create, edit, index): glass cards and tables
- templates/admin/services/index.html.twig: glass service cards
- templates/admin/status/ (index, manage): glass panels
- templates/admin/sync/index.html.twig: glass panels
- templates/admin/facturation/index.html.twig: glass tables
- templates/admin/membres.html.twig: glass form, checkboxes with esy-* group
values (esy-web, esy-mail, esy-mailer, esy-analytics, esy-monitor,
esy-defender, esy-translate, esy-signature, esy-creator, esy-aide,
esy-meet, esy-tchat, esy-ndd), Keycloak groups column in table,
available groups section
- templates/admin/stats/index.html.twig: glass KPI cards, glass-gold CA TTC,
factures emises/payees/impayees cards, services renamed to Esy-*,
rounded progress bars, bg-gray-200 track backgrounds
- templates/security/ (2fa_email, 2fa_google, forgot_password, set_password,
set_password_expired): glass headers, glass-heavy cards, input-glass
- templates/legal/ (cgu, cgv, cookie, conformite, hebergement,
mention_legal, rgpd, tarif): removed thick borders, font-black to
font-bold, text-3xl to text-2xl headings
- templates/attestation/ (verify, not_found): glass panels
- templates/espace_client/index.html.twig: glass panels
- templates/espace_prestataire/index.html.twig: glass panels
- templates/external_redirect.html.twig: glass card
- templates/status/index.html.twig: glass panels
- templates/email/base.html.twig: gradient gold header, rounded-16px
container, semi-transparent bg, soft shadow, footer address
- templates/emails/*.html.twig (9 files): removed 4px borders,
font-weight 900 to 700
- templates/pdf/*.html.twig (4 files): rounded borders, gradient header,
lighter borders
Keycloak auto-provisioning:
- src/Service/KeycloakAdminService.php: added REQUIRED_GROUPS constant
(15 groups: siteconseil_admin, siteconseil_member, esy-web, esy-mail,
esy-mailer, esy-analytics, esy-monitor, esy-defender, esy-translate,
esy-signature, esy-creator, esy-aide, esy-meet, esy-tchat, esy-ndd),
ensureRequiredGroups() method that checks existing groups and creates
missing ones, createGroup() method, getRequiredGroups() static accessor
- src/Controller/Admin/MembresController.php: calls ensureRequiredGroups()
on page load, shows flash for each auto-created group, fetches user
groups per member, passes availableGroups to template
Stats controller updated:
- src/Controller/Admin/StatsController.php: services renamed to Esy-*
(13 services), added factures_emises/payees/impayees KPI data
OAuth fix:
- src/Security/KeycloakAuthenticator.php: removed dd() debug calls,
restored flash message on auth failure with error detail
Config:
- .env: KEYCLOAK_ADMIN_CLIENT_ID=crm_siteconseil_admin, secret updated
- .env.local: same updates
- ansible/env.local.j2: KEYCLOAK_ADMIN_CLIENT_ID=crm_siteconseil_admin
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 19:34:35 +02:00
|
|
|
/**
|
|
|
|
|
* Lister tous les groupes du realm.
|
|
|
|
|
*
|
|
|
|
|
* @return list<array{id: string, name: string}>
|
|
|
|
|
*/
|
|
|
|
|
public function listGroups(): array
|
|
|
|
|
{
|
|
|
|
|
$token = $this->getToken();
|
2026-04-03 11:14:47 +02:00
|
|
|
$response = $this->httpClient->request('GET', $this->getAdminUrl(self::PATH_GROUPS), [
|
feat: complete glassmorphism redesign across all templates + Keycloak groups auto-provisioning
Templates updated to glassmorphism (40+ files):
- templates/admin/clients/ (create, index): glass cards, input-glass inputs,
btn-gold buttons, glass table headers, semi-transparent badges
- templates/admin/dashboard.html.twig: glass KPI cards
- templates/admin/profil/index.html.twig: glass form panels
- templates/admin/revendeurs/ (create, edit, index): glass cards and tables
- templates/admin/services/index.html.twig: glass service cards
- templates/admin/status/ (index, manage): glass panels
- templates/admin/sync/index.html.twig: glass panels
- templates/admin/facturation/index.html.twig: glass tables
- templates/admin/membres.html.twig: glass form, checkboxes with esy-* group
values (esy-web, esy-mail, esy-mailer, esy-analytics, esy-monitor,
esy-defender, esy-translate, esy-signature, esy-creator, esy-aide,
esy-meet, esy-tchat, esy-ndd), Keycloak groups column in table,
available groups section
- templates/admin/stats/index.html.twig: glass KPI cards, glass-gold CA TTC,
factures emises/payees/impayees cards, services renamed to Esy-*,
rounded progress bars, bg-gray-200 track backgrounds
- templates/security/ (2fa_email, 2fa_google, forgot_password, set_password,
set_password_expired): glass headers, glass-heavy cards, input-glass
- templates/legal/ (cgu, cgv, cookie, conformite, hebergement,
mention_legal, rgpd, tarif): removed thick borders, font-black to
font-bold, text-3xl to text-2xl headings
- templates/attestation/ (verify, not_found): glass panels
- templates/espace_client/index.html.twig: glass panels
- templates/espace_prestataire/index.html.twig: glass panels
- templates/external_redirect.html.twig: glass card
- templates/status/index.html.twig: glass panels
- templates/email/base.html.twig: gradient gold header, rounded-16px
container, semi-transparent bg, soft shadow, footer address
- templates/emails/*.html.twig (9 files): removed 4px borders,
font-weight 900 to 700
- templates/pdf/*.html.twig (4 files): rounded borders, gradient header,
lighter borders
Keycloak auto-provisioning:
- src/Service/KeycloakAdminService.php: added REQUIRED_GROUPS constant
(15 groups: siteconseil_admin, siteconseil_member, esy-web, esy-mail,
esy-mailer, esy-analytics, esy-monitor, esy-defender, esy-translate,
esy-signature, esy-creator, esy-aide, esy-meet, esy-tchat, esy-ndd),
ensureRequiredGroups() method that checks existing groups and creates
missing ones, createGroup() method, getRequiredGroups() static accessor
- src/Controller/Admin/MembresController.php: calls ensureRequiredGroups()
on page load, shows flash for each auto-created group, fetches user
groups per member, passes availableGroups to template
Stats controller updated:
- src/Controller/Admin/StatsController.php: services renamed to Esy-*
(13 services), added factures_emises/payees/impayees KPI data
OAuth fix:
- src/Security/KeycloakAuthenticator.php: removed dd() debug calls,
restored flash message on auth failure with error detail
Config:
- .env: KEYCLOAK_ADMIN_CLIENT_ID=crm_siteconseil_admin, secret updated
- .env.local: same updates
- ansible/env.local.j2: KEYCLOAK_ADMIN_CLIENT_ID=crm_siteconseil_admin
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 19:34:35 +02:00
|
|
|
'headers' => ['Authorization' => self::AUTH_BEARER.$token],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return $response->toArray(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return list<string>
|
|
|
|
|
*/
|
|
|
|
|
public static function getRequiredGroups(): array
|
|
|
|
|
{
|
|
|
|
|
return self::REQUIRED_GROUPS;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 15:42:52 +02:00
|
|
|
private function getGroupIdByName(string $groupName): ?string
|
|
|
|
|
{
|
|
|
|
|
$token = $this->getToken();
|
2026-04-03 11:14:47 +02:00
|
|
|
$response = $this->httpClient->request('GET', $this->getAdminUrl(self::PATH_GROUPS), [
|
2026-04-01 17:44:57 +02:00
|
|
|
'headers' => ['Authorization' => self::AUTH_BEARER.$token],
|
2026-04-01 15:42:52 +02:00
|
|
|
'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;
|
|
|
|
|
}
|
|
|
|
|
}
|