- auth/otp_setup.php : déclenche CONFIGURE_TOTP via kc_action Keycloak - Tuile OTP pointe vers otp_setup.php (plus de lien externe vers la console) - Bouton Activer en btn-primary, Reconfigurer en btn-outline - login.php : redirige vers /profile.php si déjà connecté Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
383 lines
17 KiB
PHP
383 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']) ?></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'] ?>&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'; ?>
|