- 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>
200 lines
7.1 KiB
PHP
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']})");
|
|
}
|
|
}
|