alpinux-portail/web/profile.php
Alpinux 4921a0691c Admin : gestion groupes, services par groupes, badge admin
- admin/groups.php : liste/création/suppression des groupes Keycloak
  avec comptage des membres et services associés par groupe
- admin/services.php : remplace requires_adherent par sélection multi-groupes
- inc/services.php : modèle groups[], migration auto depuis requires_adherent,
  helper service_accessible() pour l'accès contextuel
- inc/keycloak.php : kc_list_groups, kc_create_group, kc_delete_group, kc_group_members
- profile.php : badge Admin visible dans la tuile Mon compte
- index.php : utilise service_accessible() avec les groupes de l'utilisateur

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

388 lines
17 KiB
PHP

<?php
require_once __DIR__ . '/inc/config.php';
require_once __DIR__ . '/inc/auth.php';
require_once __DIR__ . '/inc/dolibarr.php';
require_once __DIR__ . '/inc/keycloak.php';
session_start_safe();
require_login();
$user = current_user();
$membership = doli_get_membership($user['email']);
$errors = [];
$edit = $_GET['edit'] ?? '';
$section = $_POST['section'] ?? '';
function geocode(string $address, string $zip, string $town, string $country = 'FR'): ?array {
$q = urlencode("$address, $zip $town, $country");
$ch = curl_init("https://nominatim.openstreetmap.org/search?q=$q&format=json&limit=1");
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5,
CURLOPT_HTTPHEADER=>['User-Agent: portail.alpinux.org']]);
$r = json_decode(curl_exec($ch), true) ?? [];
curl_close($ch);
return $r ? ['lat' => (float)$r[0]['lat'], 'lon' => (float)$r[0]['lon']] : null;
}
function save_gps(string $member_id, ?array $gps): void {
$file = preg_replace('/[^\/]+$/', 'member_gps.json', SERVICES_FILE);
$data = is_file($file) ? (json_decode(file_get_contents($file), true) ?? []) : [];
if ($gps) $data[$member_id] = $gps + ['updated' => date('Y-m-d')];
else unset($data[$member_id]);
file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
}
// ── Traitement adresse ────────────────────────────────────────────────
if ($section === 'address' && $membership) {
$address = trim($_POST['address'] ?? '');
$zip = trim($_POST['zip'] ?? '');
$town = trim($_POST['town'] ?? '');
if (!$address) $errors[] = 'L\'adresse est obligatoire.';
if (!$zip) $errors[] = 'Le code postal est obligatoire.';
if (!$town) $errors[] = 'La ville est obligatoire.';
if (!$errors) {
try {
doli_update_member($membership['id'], ['address'=>$address,'zip'=>$zip,'town'=>$town]);
$gps = geocode($address, $zip, $town, $membership['country_code'] ?? 'FR');
save_gps($membership['id'], $gps);
$membership['address'] = $address;
$membership['zip'] = $zip;
$membership['town'] = $town;
set_flash('success', 'Adresse mise à jour' . ($gps ? ' (GPS enregistré).' : '.'));
header('Location: /profile.php'); exit;
} catch (Exception $e) { $errors[] = 'Erreur : ' . $e->getMessage(); $edit = 'address'; }
} else { $edit = 'address'; }
}
// ── Traitement identité ───────────────────────────────────────────────
if ($section === 'identity') {
$new_first = trim($_POST['first_name'] ?? '');
$new_last = trim($_POST['last_name'] ?? '');
if (!$new_first) $errors[] = 'Le prénom est obligatoire.';
if (!$new_last) $errors[] = 'Le nom est obligatoire.';
if (!$errors) {
try {
kc_update_name($user['sub'], $new_first, $new_last);
if ($membership) doli_update_member($membership['id'], ['firstname' => $new_first, 'lastname' => $new_last]);
$_SESSION['user']['first_name'] = $new_first;
$_SESSION['user']['last_name'] = $new_last;
$_SESSION['user']['name'] = $new_first . ' ' . $new_last;
$user['first_name'] = $new_first;
$user['last_name'] = $new_last;
$user['name'] = $new_first . ' ' . $new_last;
set_flash('success', 'Identité mise à jour.');
header('Location: /profile.php'); exit;
} catch (Exception $e) { $errors[] = 'Erreur : ' . $e->getMessage(); $edit = 'identity'; }
} else { $edit = 'identity'; }
}
// ── Traitement email ──────────────────────────────────────────────────
if ($section === 'email') {
$new_email = trim($_POST['email'] ?? '');
$new_email2 = trim($_POST['email_confirm'] ?? '');
if (!filter_var($new_email, FILTER_VALIDATE_EMAIL)) $errors[] = 'Email invalide.';
elseif ($new_email !== $new_email2) $errors[] = 'Les deux emails ne correspondent pas.';
elseif ($new_email === $user['email']) $errors[] = 'C\'est déjà votre email actuel.';
if (!$errors) {
try {
kc_update_email($user['sub'], $new_email);
if ($membership) doli_update_member($membership['id'], ['email' => $new_email]);
$_SESSION['user']['email'] = $new_email;
$user['email'] = $new_email;
set_flash('success', 'Email mis à jour.');
header('Location: /profile.php'); exit;
} catch (Exception $e) { $errors[] = 'Erreur : ' . $e->getMessage(); $edit = 'email'; }
} else { $edit = 'email'; }
}
// ── Traitement mot de passe ───────────────────────────────────────────
if ($section === 'password') {
$pw = $_POST['password'] ?? '';
$pw2 = $_POST['password2'] ?? '';
if (!$pw) $errors[] = 'Le mot de passe ne peut pas être vide.';
elseif ($pw !== $pw2) $errors[] = 'Les mots de passe ne correspondent pas.';
elseif (strlen($pw) < 8) $errors[] = 'Le mot de passe doit contenir au moins 8 caractères.';
if (!$errors) {
try {
kc_set_password($user['sub'], $pw);
set_flash('success', 'Mot de passe modifié.');
header('Location: /profile.php'); exit;
} catch (Exception $e) { $errors[] = 'Erreur : ' . $e->getMessage(); $edit = 'password'; }
} else { $edit = 'password'; }
}
// ── Traitement OTP ────────────────────────────────────────────────────
if ($section === 'otp_delete') {
$cred_id = $_POST['credential_id'] ?? '';
if ($cred_id) {
try {
kc_delete_credential($user['sub'], $cred_id);
set_flash('success', 'OTP supprimé.');
header('Location: /profile.php'); exit;
} catch (Exception $e) { $errors[] = 'Erreur : ' . $e->getMessage(); }
}
}
// ── Données annexes ───────────────────────────────────────────────────
$gps_file = preg_replace('/[^\/]+$/', 'member_gps.json', SERVICES_FILE);
$gps = ($membership && is_file($gps_file))
? (json_decode(file_get_contents($gps_file), true)[$membership['id']] ?? null)
: null;
$otp = kc_get_otp_credential($user['sub']);
$is_adherent = $user['is_adherent']
|| ($membership && $membership['status']===1 && $membership['date_fin_ts']>time());
$title = 'Mon profil';
require __DIR__ . '/views/layout.php';
?>
<div class="profile-overview">
<!-- Identité -->
<div class="tile <?= $edit==='identity' ? 'tile-open' : '' ?>">
<div class="tile-header">
<div class="tile-label">
<div class="tile-title">Identité</div>
<div class="tile-sub"><?= htmlspecialchars(trim(($user['first_name'] ?? '') . ' ' . ($user['last_name'] ?? ''))) ?: 'Non renseignée' ?></div>
</div>
<?php if ($edit !== 'identity'): ?>
<a href="?edit=identity" class="btn-outline btn-sm tile-action">Modifier</a>
<?php endif; ?>
</div>
<div class="tile-body">
<?php if ($edit === 'identity'): ?>
<?php if ($errors): ?><div class="alert alert-error"><ul><?php foreach($errors as $e): ?><li><?= htmlspecialchars($e) ?></li><?php endforeach; ?></ul></div><?php endif; ?>
<form method="post" novalidate>
<input type="hidden" name="section" value="identity">
<div class="form-row">
<div class="form-group">
<label>Prénom</label>
<input type="text" name="first_name" required autofocus
value="<?= htmlspecialchars($_POST['first_name'] ?? $user['first_name'] ?? '') ?>">
</div>
<div class="form-group">
<label>Nom</label>
<input type="text" name="last_name" required
value="<?= htmlspecialchars($_POST['last_name'] ?? $user['last_name'] ?? '') ?>">
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn-primary">Enregistrer</button>
<a href="/profile.php" class="btn-outline">Annuler</a>
</div>
</form>
<?php else: ?>
<dl>
<dt>Prénom</dt><dd><?= htmlspecialchars($user['first_name'] ?? '—') ?></dd>
<dt>Nom</dt><dd><?= htmlspecialchars($user['last_name'] ?? '—') ?></dd>
</dl>
<?php endif; ?>
</div>
</div>
<!-- Compte -->
<div class="tile <?= $edit==='email' ? 'tile-open' : '' ?>">
<div class="tile-header">
<div class="tile-label">
<div class="tile-title">Mon compte</div>
<div class="tile-sub">
<?= htmlspecialchars($user['username']) ?>
<?php if ($user['is_admin']): ?>
<span class="badge badge-admin" style="margin-left:.4rem">Admin</span>
<?php endif; ?>
</div>
</div>
<?php if ($edit !== 'email'): ?>
<a href="?edit=email" class="btn-outline btn-sm tile-action">Modifier</a>
<?php endif; ?>
</div>
<div class="tile-body">
<?php if ($edit === 'email'): ?>
<?php if ($errors): ?><div class="alert alert-error"><ul><?php foreach($errors as $e): ?><li><?= htmlspecialchars($e) ?></li><?php endforeach; ?></ul></div><?php endif; ?>
<form method="post" novalidate>
<input type="hidden" name="section" value="email">
<div class="form-group">
<label>Email actuel</label>
<input type="email" disabled value="<?= htmlspecialchars($user['email']) ?>">
</div>
<div class="form-group">
<label>Nouvel email</label>
<input type="email" name="email" required autofocus value="<?= htmlspecialchars($_POST['email'] ?? '') ?>">
</div>
<div class="form-group">
<label>Confirmer</label>
<input type="email" name="email_confirm" required value="<?= htmlspecialchars($_POST['email_confirm'] ?? '') ?>">
</div>
<div class="form-actions">
<button type="submit" class="btn-primary">Changer l'email</button>
<a href="/profile.php" class="btn-outline">Annuler</a>
</div>
</form>
<?php else: ?>
<dl>
<dt>Email</dt><dd><?= htmlspecialchars($user['email']) ?></dd>
</dl>
<?php endif; ?>
</div>
</div>
<!-- Adhésion -->
<div class="tile">
<div class="tile-header">
<div class="tile-label">
<div class="tile-title">Adhésion</div>
<div class="tile-sub <?= $is_adherent ? 'text-success' : 'text-warning' ?>">
<?= $is_adherent ? 'Active' : 'Inactive' ?>
</div>
</div>
<a href="/helloasso/renew.php" class="btn-primary btn-sm tile-action">Renouveler</a>
</div>
<div class="tile-body">
<?php if ($membership): ?>
<dl>
<?php if ($membership['date_fin']): ?>
<dt>Valide jusqu'au</dt><dd><?= htmlspecialchars($membership['date_fin']) ?></dd>
<?php endif; ?>
<?php if ($membership['type_label']): ?>
<dt>Type</dt><dd><?= htmlspecialchars($membership['type_label']) ?></dd>
<?php endif; ?>
</dl>
<?php else: ?>
<p class="text-muted small">Aucune fiche Dolibarr trouvée.</p>
<?php endif; ?>
</div>
</div>
<!-- Adresse -->
<div class="tile <?= $edit==='address' ? 'tile-open' : '' ?>">
<div class="tile-header">
<div class="tile-label">
<div class="tile-title">Adresse postale</div>
<div class="tile-sub">
<?php if ($membership && $membership['address']): ?>
<?= htmlspecialchars($membership['zip'].' '.$membership['town']) ?>
<?php else: ?>
<span class="text-warning">Non renseignée</span>
<?php endif; ?>
</div>
</div>
<?php if ($membership && $edit !== 'address'): ?>
<a href="?edit=address" class="btn-outline btn-sm tile-action">Modifier</a>
<?php endif; ?>
</div>
<div class="tile-body">
<?php if ($edit === 'address' && $membership): ?>
<?php if ($errors): ?><div class="alert alert-error"><ul><?php foreach($errors as $e): ?><li><?= htmlspecialchars($e) ?></li><?php endforeach; ?></ul></div><?php endif; ?>
<form method="post" novalidate>
<input type="hidden" name="section" value="address">
<div class="form-group">
<label>Rue</label>
<input type="text" name="address" required autofocus placeholder="1 rue de la Paix"
value="<?= htmlspecialchars($_POST['address'] ?? $membership['address'] ?? '') ?>">
</div>
<div class="form-row">
<div class="form-group">
<label>Code postal</label>
<input type="text" name="zip" required maxlength="10"
value="<?= htmlspecialchars($_POST['zip'] ?? $membership['zip'] ?? '') ?>">
</div>
<div class="form-group">
<label>Ville</label>
<input type="text" name="town" required
value="<?= htmlspecialchars($_POST['town'] ?? $membership['town'] ?? '') ?>">
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn-primary">Enregistrer</button>
<a href="/profile.php" class="btn-outline">Annuler</a>
</div>
</form>
<?php elseif ($membership && $membership['address']): ?>
<p><?= htmlspecialchars($membership['address']) ?><br>
<?= htmlspecialchars($membership['zip'].' '.$membership['town']) ?></p>
<?php if ($gps): ?>
<p style="margin-top:.5rem">
<a href="https://www.openstreetmap.org/?mlat=<?= $gps['lat'] ?>&amp;mlon=<?= $gps['lon'] ?>#map=14/<?= $gps['lat'] ?>/<?= $gps['lon'] ?>" target="_blank" rel="noopener" class="small">Voir sur OpenStreetMap</a>
</p>
<?php endif; ?>
<?php elseif ($membership): ?>
<p class="text-muted small">Cliquez sur Modifier pour renseigner votre adresse.</p>
<?php endif; ?>
</div>
</div>
<!-- Mot de passe -->
<div class="tile <?= $edit==='password' ? 'tile-open' : '' ?>">
<div class="tile-header">
<div class="tile-label">
<div class="tile-title">Mot de passe</div>
<div class="tile-sub">AlpID</div>
</div>
<?php if ($edit !== 'password'): ?>
<a href="?edit=password" class="btn-outline btn-sm tile-action">Modifier</a>
<?php endif; ?>
</div>
<?php if ($edit === 'password'): ?>
<div class="tile-body">
<?php if ($errors): ?><div class="alert alert-error"><ul><?php foreach($errors as $e): ?><li><?= htmlspecialchars($e) ?></li><?php endforeach; ?></ul></div><?php endif; ?>
<form method="post" novalidate>
<input type="hidden" name="section" value="password">
<div class="form-group">
<label>Nouveau mot de passe</label>
<input type="password" name="password" required autofocus minlength="8" autocomplete="new-password">
</div>
<div class="form-group">
<label>Confirmer</label>
<input type="password" name="password2" required autocomplete="new-password">
</div>
<div class="form-actions">
<button type="submit" class="btn-primary">Enregistrer</button>
<a href="/profile.php" class="btn-outline">Annuler</a>
</div>
</form>
</div>
<?php endif; ?>
</div>
<!-- OTP -->
<div class="tile">
<div class="tile-header">
<div class="tile-label">
<div class="tile-title">Double authentification (OTP)</div>
<div class="tile-sub <?= $otp ? 'text-success' : 'text-warning' ?>">
<?= $otp ? 'Activée' : 'Désactivée' ?>
</div>
</div>
<a href="/auth/otp_setup.php" class="btn-<?= $otp ? 'outline' : 'primary' ?> btn-sm tile-action">
<?= $otp ? 'Reconfigurer' : 'Activer' ?>
</a>
</div>
<?php if ($otp): ?>
<div class="tile-body">
<p class="small text-muted" style="margin-bottom:.8rem">
Configurée le <?= date('d/m/Y', (int)($otp['createdDate'] / 1000)) ?>
</p>
<form method="post" onsubmit="return confirm('Supprimer l\'OTP ? Votre compte sera moins sécurisé.')">
<input type="hidden" name="section" value="otp_delete">
<input type="hidden" name="credential_id" value="<?= htmlspecialchars($otp['id']) ?>">
<button type="submit" class="btn-danger btn-sm">Supprimer l'OTP</button>
</form>
</div>
<?php endif; ?>
</div>
<!-- Connexions -->
<div class="tile">
<div class="tile-header">
<div class="tile-label">
<div class="tile-title">Connexions</div>
<div class="tile-sub">Historique et statistiques</div>
</div>
<a href="/history.php" class="btn-outline btn-sm tile-action">Voir</a>
</div>
</div>
</div>
<?php require __DIR__ . '/views/layout_end.php'; ?>