alpinux-portail/web/inc/keycloak.php
Alpinux fcdd094e26 Portail membres complet : profil, adhésion, historique, OTP
- Inscription sans redirection HelloAsso (acte volontaire séparé)
- Génération automatique d'identifiant AlpID (prenom.code mnémotechnique)
- Profil en tuiles : identité, compte, mot de passe, OTP, adhésion, adresse, connexions
- Double authentification : activation/suppression OTP via Keycloak
- Page d'accueil contextuelle (bienvenue si connecté, CTA adhésion si non adhérent)
- Historique des connexions avec statistiques et graphiques Chart.js
- Géocodage Nominatim + lien OpenStreetMap pour l'adresse
- HelloAsso : checkout intent, validation paiement, mise à jour Dolibarr

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 00:43:38 +02:00

200 lines
7.1 KiB
PHP

<?php
require_once __DIR__ . '/config.php';
class KcUserExistsException extends RuntimeException {}
function _kc_admin_token(): string {
static $cache = null;
if ($cache && $cache['expires'] > time()) return $cache['token'];
$ch = curl_init(ALPID_BASE . '/realms/master/protocol/openid-connect/token');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query([
'grant_type' => 'client_credentials',
'client_id' => KC_SERVICE_CLIENT_ID,
'client_secret' => KC_SERVICE_CLIENT_SECRET,
]),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) {
throw new RuntimeException("Authentification Keycloak Admin échouée ($status)");
}
$data = json_decode($body, true);
$cache = ['token' => $data['access_token'], 'expires' => time() + $data['expires_in'] - 10];
return $cache['token'];
}
function _kc_request(string $method, string $path, mixed $body = null): array {
$url = KC_ADMIN_BASE . $path;
$token = _kc_admin_token();
$headers = ["Authorization: Bearer $token", "Content-Type: application/json"];
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_HEADER => true,
]);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
curl_close($ch);
$resp_headers = substr($response, 0, $header_size);
$resp_body = substr($response, $header_size);
return ['status' => $status, 'headers' => $resp_headers, 'body' => $resp_body];
}
function _kc_encode_username(string $lastname): string {
$name = strtolower(iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $lastname));
$name = preg_replace('/[^a-z]/', '', $name);
$len = strlen($name);
if ($len <= 2) return $name;
return $name[0] . ($len - 2) . $name[$len - 1];
}
function kc_username_exists(string $username): bool {
$resp = _kc_request('GET', '/users?username=' . urlencode($username) . '&exact=true');
if ($resp['status'] !== 200) return false;
return count(json_decode($resp['body'], true) ?? []) > 0;
}
function kc_generate_username(string $lastname, string $firstname): string {
$fn = strtolower(preg_replace('/[^a-z]/i', '', iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $firstname)));
$code = _kc_encode_username($lastname);
$base = $fn . '.' . $code;
if (!kc_username_exists($base)) return $base;
for ($i = 2; $i <= 99; $i++) {
$try = $base . $i;
if (!kc_username_exists($try)) return $try;
}
return $base . uniqid('', false);
}
function kc_create_user(string $username, string $email, string $first_name, string $last_name, string $password): string {
$resp = _kc_request('POST', '/users', [
'username' => $username,
'email' => $email,
'firstName' => $first_name,
'lastName' => $last_name,
'enabled' => true,
'emailVerified' => false,
'credentials' => [['type' => 'password', 'value' => $password, 'temporary' => false]],
]);
if ($resp['status'] === 409) {
$detail = json_decode($resp['body'], true)['errorMessage'] ?? '';
$msg = str_contains(strtolower($detail), 'email')
? 'Cet email est déjà associé à un compte existant.'
: 'Ce nom d\'utilisateur est déjà utilisé.';
throw new KcUserExistsException($msg);
}
if ($resp['status'] !== 201) {
throw new RuntimeException("Erreur création compte ({$resp['status']}) : {$resp['body']}");
}
// L'ID est dans le header Location
preg_match('#/users/([a-f0-9-]+)#', $resp['headers'], $m);
return $m[1] ?? '';
}
function kc_get_otp_credential(string $user_id): ?array {
$resp = _kc_request('GET', "/users/$user_id/credentials");
if ($resp['status'] !== 200) return null;
foreach (json_decode($resp['body'], true) ?? [] as $c) {
if ($c['type'] === 'otp') return $c;
}
return null;
}
function kc_delete_credential(string $user_id, string $credential_id): void {
$resp = _kc_request('DELETE', "/users/$user_id/credentials/$credential_id");
if ($resp['status'] >= 300) {
throw new RuntimeException("Erreur suppression credential ({$resp['status']})");
}
}
function kc_update_name(string $user_id, string $first_name, string $last_name): void {
$resp = _kc_request('PUT', "/users/$user_id", [
'firstName' => $first_name,
'lastName' => $last_name,
]);
if ($resp['status'] >= 300) {
throw new RuntimeException("Erreur mise à jour nom ({$resp['status']})");
}
}
function kc_update_email(string $user_id, string $email): void {
$resp = _kc_request('PUT', "/users/$user_id", [
'email' => $email,
'emailVerified' => false,
]);
if ($resp['status'] >= 300) {
throw new RuntimeException("Erreur mise à jour email ({$resp['status']})");
}
}
function kc_set_password(string $user_id, string $password): void {
$resp = _kc_request('PUT', "/users/$user_id/reset-password", [
'type' => 'password',
'value' => $password,
'temporary' => false,
]);
if ($resp['status'] >= 300) {
throw new RuntimeException("Erreur changement mot de passe ({$resp['status']})");
}
}
function kc_list_users(int $max = 200): array {
$resp = _kc_request('GET', '/users?max=' . $max);
if ($resp['status'] !== 200) return [];
$users = json_decode($resp['body'], true) ?? [];
foreach ($users as &$user) {
$gr = _kc_request('GET', "/users/{$user['id']}/groups");
$user['groupNames'] = $gr['status'] === 200
? array_column(json_decode($gr['body'], true) ?? [], 'name')
: [];
}
return $users;
}
function _kc_group_id(string $group_name): string {
$resp = _kc_request('GET', '/groups?search=' . urlencode($group_name));
$groups = json_decode($resp['body'], true) ?? [];
foreach ($groups as $g) {
if ($g['name'] === $group_name) return $g['id'];
}
throw new RuntimeException("Groupe « $group_name » introuvable dans Keycloak.");
}
function kc_add_to_group(string $user_id, string $group_name): void {
$group_id = _kc_group_id($group_name);
$resp = _kc_request('PUT', "/users/$user_id/groups/$group_id");
if ($resp['status'] >= 300) {
throw new RuntimeException("Erreur ajout groupe ({$resp['status']})");
}
}
function kc_remove_from_group(string $user_id, string $group_name): void {
$group_id = _kc_group_id($group_name);
$resp = _kc_request('DELETE', "/users/$user_id/groups/$group_id");
if ($resp['status'] >= 300) {
throw new RuntimeException("Erreur suppression groupe ({$resp['status']})");
}
}