Compare commits

..

No commits in common. "fcdd094e26c4ecb226bf15270d7f8dc8413c5ca7" and "8e32cd9c07fad7265cc29696a8af368379435971" have entirely different histories.

37 changed files with 0 additions and 2711 deletions

View file

@ -1,41 +0,0 @@
# ── Clé secrète PHP sessions ──────────────────────────────────────────
SECRET_KEY=changez-moi-valeur-aleatoire-longue
# ── AlpID OIDC (client portail dans le realm alpinux) ─────────────────
ALPID_BASE=https://alpid.alpinux.org
ALPID_REALM=alpinux
ALPID_CLIENT_ID=portail
ALPID_CLIENT_SECRET=PortailAlpinux8281a984405f7c88d9b0
# ── Keycloak Admin REST API — compte de service (realm master) ────────
KC_SERVICE_CLIENT_ID=portail-service
KC_SERVICE_CLIENT_SECRET=
# ── Dolibarr REST API ──────────────────────────────────────────────────
DOLIBARR_URL=https://dolibarr.alpinux.org
DOLIBARR_API_KEY=
# ── Application ───────────────────────────────────────────────────────
APP_URL=https://portail.alpinux.org
HELLOASSO_URL=https://www.helloasso.com/associations/alpinux-le-lug-de-savoie/adhesions/adhesions-a-l-annee
# ── HelloAsso API ──────────────────────────────────────────────────────
HA_CLIENT_ID=
HA_CLIENT_SECRET=
HA_ORG_SLUG=alpinux-le-lug-de-savoie
HA_FORM_SLUG=adhesions-a-l-annee
HA_AMOUNT=1500
HA_ITEM_NAME=Adhésion Alpinux
# ── Groupes Keycloak ──────────────────────────────────────────────────
ADMIN_GROUPS=admins
ADHERENT_GROUP=adherents
# ── Fichier JSON de config des services ───────────────────────────────
# Par défaut : /var/www/clients/client1/web16/../services.json (hors web root)
SERVICES_FILE=/var/www/clients/client1/web16/private/services.json
# ── Déploiement (scripts/deploy.sh) ───────────────────────────────────
PORTAIL_HOST=alpinux.org
PORTAIL_PATH=/var/www/clients/client1/web16/web
# PORTAIL_USER=abonnelc # laisser vide pour utiliser l'alias SSH ~/.ssh/config

2
.gitignore vendored
View file

@ -1,2 +0,0 @@
.env
web/inc/config.local.php

View file

@ -14,10 +14,3 @@ Géré directement via **ISPConfig** sur le serveur. Le contenu de ce sous-domai
## Notes ## Notes
Ce dossier est un espace réservé. Si un développement spécifique au portail est nécessaire (thème, plugin, scripts de déploiement), il sera ajouté ici. Ce dossier est un espace réservé. Si un développement spécifique au portail est nécessaire (thème, plugin, scripts de déploiement), il sera ajouté ici.
---
## Voir aussi
Vue d'ensemble des projets, procédures de déploiement globales et accès :
`~/Projects/org.alpinux.owni/README.md`

View file

@ -1,105 +0,0 @@
#!/usr/bin/env bash
# deploy.sh — synchronise le portail PHP vers portail.alpinux.org
#
# Usage :
# ./deploy.sh # aperçu + confirmation
# ./deploy.sh -y # pousse sans confirmation
# ./deploy.sh -n # dry-run seulement
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="$SCRIPT_DIR/../.env"
RED='\033[0;31m'; YELLOW='\033[1;33m'; GREEN='\033[0;32m'
CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m'
if [ ! -f "$ENV_FILE" ]; then
echo -e "${RED}Erreur : fichier .env introuvable.${RESET}"
echo "Copier .env.example en .env et renseigner les valeurs."
exit 1
fi
# shellcheck source=/dev/null
source "$ENV_FILE"
LOCAL_DIR="$SCRIPT_DIR/../web"
REMOTE_HOST="${PORTAIL_HOST:-alpinux.org}"
REMOTE_PATH="${PORTAIL_PATH:-/var/www/clients/client1/web16/web}"
if [ -n "${PORTAIL_USER:-}" ]; then
REMOTE="${PORTAIL_USER}@${REMOTE_HOST}:${REMOTE_PATH}/"
else
REMOTE="${REMOTE_HOST}:${REMOTE_PATH}/"
fi
DRY_RUN=false; AUTO_YES=false
for arg in "$@"; do
case "$arg" in
-n|--dry-run) DRY_RUN=true ;;
-y|--yes) AUTO_YES=true ;;
esac
done
EXCLUDES=(
--exclude='.git/'
--exclude='.env'
--exclude='*.log'
# Fichiers gérés par ISPConfig — ne pas toucher
--exclude='error/'
--exclude='stats/'
--exclude='standard_index.html'
)
echo -e "${BOLD}Analyse des changements…${RESET}"
echo -e " Source : ${CYAN}$LOCAL_DIR/${RESET}"
echo -e " Cible : ${CYAN}$REMOTE${RESET}"
echo ""
DIFF=$(rsync -rlcz --dry-run --itemize-changes --delete \
--rsync-path="sudo rsync" \
"${EXCLUDES[@]}" \
"$LOCAL_DIR/" "$REMOTE" 2>&1)
NEW=0; CHANGED=0; DELETED=0
while IFS= read -r line; do
item="${line:0:11}"; file="${line:12}"
[ -z "$file" ] && continue
if [[ "$item" == *"deleting"* ]]; then echo -e " ${RED}supprimé ${RESET}$file"; DELETED=$((DELETED+1))
elif [[ "$item" =~ ^\<f\+{6,} ]]; then echo -e " ${GREEN}nouveau ${RESET}$file"; NEW=$((NEW+1))
elif [[ "$item" =~ ^\<f ]]; then echo -e " ${YELLOW}modifié ${RESET}$file"; CHANGED=$((CHANGED+1))
fi
done <<< "$DIFF"
TOTAL=$((NEW+CHANGED+DELETED))
if [ "$TOTAL" -eq 0 ]; then
echo -e "${GREEN}Tout est à jour.${RESET}"; exit 0
fi
echo ""
echo -e " ${GREEN}+$NEW nouveau(x)${RESET} ${YELLOW}~$CHANGED modifié(s)${RESET} ${RED}-$DELETED supprimé(s)${RESET}"
echo ""
if $DRY_RUN; then echo -e "${CYAN}Mode dry-run — aucune modification.${RESET}"; exit 0; fi
if ! $AUTO_YES; then
read -rp "Pousser vers $REMOTE_HOST ? [o/N] " confirm
[[ "$confirm" =~ ^[oOyY]$ ]] || { echo "Annulé."; exit 0; }
fi
DELETE_FLAG=()
if [ "$DELETED" -gt 0 ]; then
echo -e "${RED}${BOLD}$DELETED fichier(s) seront supprimés du serveur.${RESET}"
read -rp "Confirmer les suppressions ? [o/N] " confirm_del
[[ "$confirm_del" =~ ^[oOyY]$ ]] && DELETE_FLAG=(--delete)
fi
echo ""
echo -e "${BOLD}Synchronisation…${RESET}"
rsync -rlcz --human-readable --progress \
--rsync-path="sudo rsync" \
"${DELETE_FLAG[@]}" \
"${EXCLUDES[@]}" \
"$LOCAL_DIR/" "$REMOTE"
echo ""
echo -e "${GREEN}✓ Déploiement terminé.${RESET}"

View file

@ -1,9 +0,0 @@
# Directives Apache personnalisées pour portail.alpinux.org
# À coller dans ISPConfig : Sites > portail.alpinux.org > onglet "Options" > "Apache Directives"
# (valables pour les blocs HTTP et HTTPS)
# Supprime la config admin Flask obsolète (n'est plus sur ce vhost)
# ProxyPass /admin/ ... — à retirer d'ISPConfig si encore présent
# Le site PHP est servi directement par PHP-FPM — aucun ProxyPass nécessaire.
# S'assurer que ISPConfig est configuré avec PHP 8.1 pour ce site (web16).

View file

@ -1,6 +0,0 @@
Options -Indexes
# Fichiers sensibles
<FilesMatch "^\.env">
Require all denied
</FilesMatch>

View file

@ -1,110 +0,0 @@
<?php
require_once __DIR__ . '/../inc/config.php';
require_once __DIR__ . '/../inc/auth.php';
require_once __DIR__ . '/../inc/keycloak.php';
session_start_safe();
require_admin();
$users = [];
$kc_error = null;
try {
$users = kc_list_users();
} catch (Exception $e) {
$kc_error = $e->getMessage();
}
// Sépare adhérents et inscrits en attente
$adherents = array_filter($users, fn($u) => in_array(ADHERENT_GROUP, $u['groupNames'], true));
$pending = array_filter($users, fn($u) => !in_array(ADHERENT_GROUP, $u['groupNames'], true));
$title = 'Gestion des membres';
require __DIR__ . '/../views/layout.php';
?>
<div class="admin-page">
<div class="page-header">
<h1>Gestion des membres</h1>
<div class="admin-nav">
<a href="/admin/members.php" class="active">Membres</a>
<a href="/admin/services.php">Services</a>
</div>
</div>
<?php if ($kc_error): ?>
<div class="alert alert-error">Erreur Keycloak : <?= htmlspecialchars($kc_error) ?></div>
<?php endif; ?>
<!-- Inscrits en attente de validation -->
<section class="card">
<h2>En attente de validation (<?= count($pending) ?>)</h2>
<?php if (!$pending): ?>
<p class="text-muted">Aucun inscrit en attente.</p>
<?php else: ?>
<table class="members-table">
<thead>
<tr>
<th>Nom</th><th>Identifiant</th><th>Email</th><th>Groupes</th><th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($pending as $u): ?>
<tr>
<td><?= htmlspecialchars(trim(($u['firstName'] ?? '') . ' ' . ($u['lastName'] ?? ''))) ?></td>
<td><?= htmlspecialchars($u['username'] ?? '') ?></td>
<td><?= htmlspecialchars($u['email'] ?? '') ?></td>
<td><?= htmlspecialchars(implode(', ', $u['groupNames']) ?: '—') ?></td>
<td>
<form method="post" action="/admin/validate.php" style="display:inline">
<input type="hidden" name="user_id" value="<?= htmlspecialchars($u['id']) ?>">
<button type="submit" class="btn-primary btn-sm"
onclick="return confirm('Valider l\'adhésion de <?= htmlspecialchars(addslashes($u['username'] ?? '')) ?> ?')">
Valider adhésion
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</section>
<!-- Adhérents actifs -->
<section class="card">
<h2>Adhérents (<?= count($adherents) ?>)</h2>
<?php if (!$adherents): ?>
<p class="text-muted">Aucun adhérent.</p>
<?php else: ?>
<table class="members-table">
<thead>
<tr>
<th>Nom</th><th>Identifiant</th><th>Email</th><th>Groupes</th><th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($adherents as $u): ?>
<tr>
<td><?= htmlspecialchars(trim(($u['firstName'] ?? '') . ' ' . ($u['lastName'] ?? ''))) ?></td>
<td><?= htmlspecialchars($u['username'] ?? '') ?></td>
<td><?= htmlspecialchars($u['email'] ?? '') ?></td>
<td><?= htmlspecialchars(implode(', ', $u['groupNames'])) ?></td>
<td>
<form method="post" action="/admin/revoke.php" style="display:inline">
<input type="hidden" name="user_id" value="<?= htmlspecialchars($u['id']) ?>">
<button type="submit" class="btn-danger btn-sm"
onclick="return confirm('Révoquer l\'adhésion de <?= htmlspecialchars(addslashes($u['username'] ?? '')) ?> ?')">
Révoquer
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</section>
</div>
<?php require __DIR__ . '/../views/layout_end.php'; ?>

View file

@ -1,22 +0,0 @@
<?php
require_once __DIR__ . '/../inc/config.php';
require_once __DIR__ . '/../inc/auth.php';
require_once __DIR__ . '/../inc/keycloak.php';
session_start_safe();
require_admin();
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || empty($_POST['user_id'])) {
header('Location: /admin/members.php');
exit;
}
try {
kc_remove_from_group($_POST['user_id'], ADHERENT_GROUP);
set_flash('success', 'Adhésion révoquée.');
} catch (Exception $e) {
set_flash('error', 'Erreur : ' . $e->getMessage());
}
header('Location: /admin/members.php');
exit;

View file

@ -1,100 +0,0 @@
<?php
require_once __DIR__ . '/../inc/config.php';
require_once __DIR__ . '/../inc/auth.php';
require_once __DIR__ . '/../inc/services.php';
session_start_safe();
require_admin();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$updated = [];
foreach ($_POST as $key => $val) {
if (!str_starts_with($key, 'name_')) continue;
$idx = substr($key, 5);
$updated[] = [
'name' => trim($val),
'url' => trim($_POST["url_$idx"] ?? ''),
'description' => trim($_POST["description_$idx"] ?? ''),
'requires_adherent' => isset($_POST["requires_$idx"]),
'visible' => isset($_POST["visible_$idx"]),
];
}
// Ajout d'un nouveau service si rempli
$new_name = trim($_POST['new_name'] ?? '');
if ($new_name) {
$updated[] = [
'name' => $new_name,
'url' => trim($_POST['new_url'] ?? ''),
'description' => trim($_POST['new_description'] ?? ''),
'requires_adherent' => isset($_POST['new_requires']),
'visible' => isset($_POST['new_visible']),
];
}
services_save($updated);
set_flash('success', 'Configuration des services sauvegardée.');
header('Location: /admin/services.php');
exit;
}
$services = services_list();
$title = 'Paramétrage des services';
require __DIR__ . '/../views/layout.php';
?>
<div class="admin-page">
<div class="page-header">
<h1>Paramétrage des services</h1>
<div class="admin-nav">
<a href="/admin/members.php">Membres</a>
<a href="/admin/services.php" class="active">Services</a>
</div>
</div>
<div class="card">
<p class="text-muted">
Définissez quels services sont accessibles aux simples inscrits
et lesquels nécessitent une adhésion validée.
</p>
<form method="post">
<table class="services-table">
<thead>
<tr>
<th>Nom</th>
<th>URL</th>
<th>Description</th>
<th>Adhérent requis</th>
<th>Visible</th>
</tr>
</thead>
<tbody>
<?php foreach ($services as $i => $s): ?>
<tr>
<td><input type="text" name="name_<?= $i ?>" value="<?= htmlspecialchars($s['name']) ?>" required></td>
<td><input type="url" name="url_<?= $i ?>" value="<?= htmlspecialchars($s['url']) ?>"></td>
<td><input type="text" name="description_<?= $i ?>" value="<?= htmlspecialchars($s['description']) ?>"></td>
<td class="center">
<input type="checkbox" name="requires_<?= $i ?>" <?= $s['requires_adherent'] ? 'checked' : '' ?>>
</td>
<td class="center">
<input type="checkbox" name="visible_<?= $i ?>" <?= $s['visible'] ? 'checked' : '' ?>>
</td>
</tr>
<?php endforeach; ?>
<!-- Ligne ajout nouveau service -->
<tr class="new-row">
<td><input type="text" name="new_name" placeholder="Nouveau service"></td>
<td><input type="url" name="new_url" placeholder="https://..."></td>
<td><input type="text" name="new_description" placeholder="Description"></td>
<td class="center"><input type="checkbox" name="new_requires"></td>
<td class="center"><input type="checkbox" name="new_visible" checked></td>
</tr>
</tbody>
</table>
<button type="submit" class="btn-primary">Enregistrer</button>
</form>
</div>
</div>
<?php require __DIR__ . '/../views/layout_end.php'; ?>

View file

@ -1,22 +0,0 @@
<?php
require_once __DIR__ . '/../inc/config.php';
require_once __DIR__ . '/../inc/auth.php';
require_once __DIR__ . '/../inc/keycloak.php';
session_start_safe();
require_admin();
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || empty($_POST['user_id'])) {
header('Location: /admin/members.php');
exit;
}
try {
kc_add_to_group($_POST['user_id'], ADHERENT_GROUP);
set_flash('success', 'Membre validé — groupe « ' . ADHERENT_GROUP . ' » assigné.');
} catch (Exception $e) {
set_flash('error', 'Erreur : ' . $e->getMessage());
}
header('Location: /admin/members.php');
exit;

View file

@ -1,349 +0,0 @@
/* ── Reset & base ─────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #f5f7fa;
--bg2: #ffffff;
--bg3: #eef0f5;
--border: #d5d9e8;
--primary: #3b5bdb;
--primary-h: #2f4ac4;
--success: #2b9e5e;
--warning: #d08a00;
--danger: #c0392b;
--text: #1a1d2e;
--text-muted:#6b7289;
--radius: 8px;
--shadow: 0 2px 16px rgba(0,0,0,.08);
}
body {
font-family: 'Segoe UI', system-ui, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
flex-direction: column;
line-height: 1.6;
}
a { color: var(--primary); text-decoration: none; }
a:hover { color: var(--primary-h); }
/* ── Header / Nav ─────────────────────────────────────────────────── */
header {
background: var(--bg2);
border-bottom: 1px solid var(--border);
padding: 0 2rem;
position: sticky; top: 0; z-index: 100;
}
nav {
max-width: 1100px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
height: 60px;
}
.brand {
display: flex; align-items: center; gap: .6rem;
font-weight: 600; font-size: 1rem; color: var(--text);
}
.brand img { height: 28px; }
.nav-links { display: flex; align-items: center; gap: 1rem; }
.nav-links a { color: var(--text-muted); font-size: .9rem; }
.nav-links a:hover { color: var(--text); }
/* ── Buttons ──────────────────────────────────────────────────────── */
.btn-primary, .btn-outline, .btn-danger {
display: inline-block;
padding: .5rem 1.2rem;
border-radius: var(--radius);
font-size: .9rem;
font-weight: 500;
cursor: pointer;
border: none;
transition: opacity .15s;
}
.btn-primary { background: var(--primary); color: #fff; }
.btn-outline { background: transparent; border: 1px solid var(--border); color: var(--text); }
.btn-danger { background: var(--danger); color: #fff; }
.btn-full { width: 100%; text-align: center; padding: .7rem; }
.btn-sm { padding: .3rem .8rem; font-size: .8rem; }
.btn-primary:hover, .btn-danger:hover { opacity: .85; }
.btn-outline:hover { border-color: var(--primary); color: var(--primary); }
/* ── Main ─────────────────────────────────────────────────────────── */
main {
flex: 1;
max-width: 1100px;
width: 100%;
margin: 0 auto;
padding: 2rem;
}
/* ── Flash ────────────────────────────────────────────────────────── */
.flash {
padding: .8rem 1.2rem;
border-radius: var(--radius);
margin-bottom: 1.5rem;
font-size: .9rem;
}
.flash-success { background: #eaf7f0; border: 1px solid var(--success); color: var(--success); }
.flash-error { background: #fdecea; border: 1px solid var(--danger); color: var(--danger); }
/* ── Cards ────────────────────────────────────────────────────────── */
.card {
background: var(--bg2);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 2rem;
margin-bottom: 1.5rem;
max-width: 600px;
}
.card.center { text-align: center; }
.card h1, .card h2 { margin-bottom: 1rem; font-size: 1.3rem; }
/* ── Alerts ───────────────────────────────────────────────────────── */
.alert { padding: .8rem 1rem; border-radius: var(--radius); margin-bottom: 1rem; font-size: .9rem; }
.alert-error { background: #fdecea; border: 1px solid var(--danger); color: var(--danger); }
.alert-success { background: #eaf7f0; border: 1px solid var(--success); color: var(--success); }
.alert ul { margin: 0; padding-left: 1.2rem; }
/* ── Forms ────────────────────────────────────────────────────────── */
.form-group { margin-bottom: 1.2rem; }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
label { display: block; font-size: .85rem; color: var(--text-muted); margin-bottom: .4rem; }
input[type=text], input[type=email], input[type=password], input[type=url] {
width: 100%;
background: var(--bg3);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text);
padding: .55rem .8rem;
font-size: .95rem;
transition: border-color .15s;
}
input:focus { outline: none; border-color: var(--primary); }
.form-footer { text-align: center; margin-top: 1rem; font-size: .85rem; color: var(--text-muted); }
.username-hint {
background: #eef1ff;
border: 1px solid var(--primary);
border-radius: var(--radius);
padding: .5rem .9rem;
font-size: .85rem;
color: var(--text);
margin-bottom: 1.2rem;
}
.subtitle { color: var(--text-muted); font-size: .9rem; margin-bottom: 1.5rem; }
/* ── Hero ─────────────────────────────────────────────────────────── */
.hero {
text-align: center;
padding: 4rem 1rem 3rem;
}
.hero h1 { font-size: 2rem; margin-bottom: .8rem; }
.hero p { color: var(--text-muted); font-size: 1.1rem; margin-bottom: 2rem; }
.hero-actions { display: flex; justify-content: center; gap: 1rem; flex-wrap: wrap; }
/* ── Home about ───────────────────────────────────────────────────── */
.home-about { margin: 0 0 2.5rem; }
.about-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 1rem;
}
.about-card {
background: var(--bg2);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.2rem 1.4rem;
}
.about-title { font-weight: 600; font-size: .9rem; margin-bottom: .5rem; }
.about-card p { font-size: .85rem; color: var(--text-muted); line-height: 1.6; }
/* ── Services grid ────────────────────────────────────────────────── */
.services-grid h2, .services-section h2 {
font-size: 1.2rem; margin-bottom: 1rem;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 1rem;
}
.service-card {
background: var(--bg2);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.2rem;
}
.service-card.locked { opacity: .5; }
.service-card.adherent-only { border-color: var(--primary); }
.service-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: .5rem; }
.service-card p { font-size: .85rem; color: var(--text-muted); margin: .5rem 0; }
/* ── Badges ───────────────────────────────────────────────────────── */
.badge {
font-size: .7rem; font-weight: 600;
padding: .2rem .5rem;
border-radius: 4px;
background: #dde4ff;
color: var(--primary);
}
.badge-inscrit { background: var(--bg3); color: var(--text-muted); }
.badge-adherent { background: #d4f0e4; color: var(--success); }
.badge-admin { background: #fef3d0; color: var(--warning); }
/* ── Dashboard ────────────────────────────────────────────────────── */
.dashboard-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.5rem; }
.dashboard-header h1 { font-size: 1.5rem; }
.badges { display: flex; gap: .5rem; }
.dashboard-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 2rem; }
.dashboard-grid .card { max-width: none; }
.services-section { margin-top: 1rem; }
dl { display: grid; grid-template-columns: auto 1fr; gap: .4rem 1rem; margin-bottom: 1rem; }
dt { color: var(--text-muted); font-size: .85rem; }
dd { font-size: .9rem; }
/* ── Steps (inscription réussie) ──────────────────────────────────── */
.success-icon {
width: 60px; height: 60px;
background: #d4f0e4;
color: var(--success);
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 1.8rem;
margin: 0 auto 1.5rem;
}
.steps { text-align: left; margin: 1.5rem 0; display: flex; flex-direction: column; gap: 1.2rem; }
.step { display: flex; gap: 1rem; align-items: flex-start; }
.step-num {
width: 28px; height: 28px; flex-shrink: 0;
border-radius: 50%;
background: var(--bg3); border: 2px solid var(--border);
display: flex; align-items: center; justify-content: center;
font-size: .85rem; font-weight: 700;
}
.step.done .step-num { background: var(--success); border-color: var(--success); color: #fff; }
.step strong { display: block; margin-bottom: .2rem; }
.step p { font-size: .85rem; color: var(--text-muted); margin: .3rem 0; }
/* ── Admin ────────────────────────────────────────────────────────── */
.admin-page { max-width: 1000px; }
.page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.5rem; }
.page-header h1 { font-size: 1.4rem; }
.admin-nav { display: flex; gap: .5rem; }
.admin-nav a {
padding: .4rem .9rem; border-radius: var(--radius);
font-size: .85rem; color: var(--text-muted);
border: 1px solid transparent;
}
.admin-nav a.active, .admin-nav a:hover { border-color: var(--border); color: var(--text); }
.admin-page .card { max-width: none; }
.admin-page .card h2 { font-size: 1rem; margin-bottom: 1rem; color: var(--text-muted); }
table { width: 100%; border-collapse: collapse; font-size: .85rem; }
th { text-align: left; padding: .5rem .7rem; color: var(--text-muted); border-bottom: 1px solid var(--border); }
td { padding: .6rem .7rem; border-bottom: 1px solid var(--border); }
td.center { text-align: center; }
.members-table tr:last-child td { border-bottom: none; }
.services-table input[type=text], .services-table input[type=url] {
padding: .3rem .5rem; font-size: .85rem;
}
.new-row td { background: #eef1ff; }
/* ── Helpers ──────────────────────────────────────────────────────── */
.text-success { color: var(--success); }
.text-warning { color: var(--warning); }
.text-muted { color: var(--text-muted); }
.small { font-size: .8rem; }
/* ── Profile tuiles ───────────────────────────────────────────────── */
.profile-overview {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1.2rem;
align-items: start;
}
.tile {
background: var(--bg2);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
box-shadow: var(--shadow);
}
.tile-open { border-color: var(--primary); }
.tile-header {
display: flex;
align-items: center;
gap: .8rem;
padding: 1rem 1.2rem;
}
.tile-icon { font-size: 1.4rem; flex-shrink: 0; }
.tile-title { font-weight: 600; font-size: .95rem; }
.tile-sub { font-size: .8rem; color: var(--text-muted); margin-top: .1rem; }
.tile-action { margin-left: auto; flex-shrink: 0; }
.tile-body {
padding: 0 1.2rem 1.2rem;
border-top: 1px solid var(--border);
padding-top: .8rem;
}
/* ── History ──────────────────────────────────────────────────────── */
.history-page { max-width: 1100px; }
.history-page h1 { font-size: 1.4rem; }
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
}
.stat-card {
background: var(--bg2);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1rem;
text-align: center;
}
.stat-warning { border-color: var(--danger); }
.stat-value { font-size: 2rem; font-weight: 700; color: var(--primary); line-height: 1.1; }
.stat-warning .stat-value { color: var(--danger); }
.stat-label { font-size: .75rem; color: var(--text-muted); margin-top: .3rem; }
.charts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(440px, 1fr));
gap: 1.2rem;
margin-bottom: 1.5rem;
}
.chart-card { max-width: none; }
.chart-card h2 { font-size: .9rem; color: var(--text-muted); margin-bottom: .8rem; }
.ip-bars { display: flex; flex-direction: column; gap: .5rem; margin-top: .5rem; }
.ip-bar-row { display: flex; align-items: center; gap: .6rem; font-size: .8rem; }
.ip-label { width: 130px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-muted); }
.ip-bar-wrap { flex: 1; background: var(--bg3); border-radius: 4px; height: 10px; }
.ip-bar { background: var(--primary); height: 10px; border-radius: 4px; min-width: 4px; }
.ip-count { width: 24px; text-align: right; font-weight: 600; }
/* ── Footer ───────────────────────────────────────────────────────── */
footer {
text-align: center;
padding: 1.5rem;
font-size: .8rem;
color: var(--text-muted);
border-top: 1px solid var(--border);
margin-top: auto;
}
footer a { color: var(--text-muted); }
footer a:hover { color: var(--text); }
/* ── Responsive ───────────────────────────────────────────────────── */
@media (max-width: 700px) {
.dashboard-grid { grid-template-columns: 1fr; }
.form-row { grid-template-columns: 1fr; }
.page-header { flex-direction: column; align-items: flex-start; gap: .8rem; }
}

View file

@ -1,52 +0,0 @@
<?php
require_once __DIR__ . '/../inc/config.php';
require_once __DIR__ . '/../inc/auth.php';
require_once __DIR__ . '/../inc/oidc.php';
session_start_safe();
// Vérification state CSRF
$state = $_GET['state'] ?? '';
if (!$state || $state !== ($_SESSION['oidc_state'] ?? '')) {
http_response_code(400);
exit('Erreur : state OIDC invalide.');
}
unset($_SESSION['oidc_state'], $_SESSION['oidc_nonce']);
$code = $_GET['code'] ?? '';
if (!$code) {
$error = $_GET['error_description'] ?? $_GET['error'] ?? 'Connexion annulée.';
header('Location: /?error=' . urlencode($error));
exit;
}
try {
$tokens = oidc_exchange_code($code);
$userinfo = oidc_userinfo($tokens['access_token']);
$groups = $userinfo['groups'] ?? [];
$is_admin = (bool)array_intersect(ADMIN_GROUPS, $groups);
$is_adherent = in_array(ADHERENT_GROUP, $groups, true) || $is_admin;
$_SESSION['user'] = [
'sub' => $userinfo['sub'],
'name' => $userinfo['name'] ?? $userinfo['preferred_username'] ?? '',
'first_name' => $userinfo['given_name'] ?? '',
'last_name' => $userinfo['family_name'] ?? '',
'email' => $userinfo['email'] ?? '',
'username' => $userinfo['preferred_username'] ?? '',
'groups' => $groups,
'is_admin' => $is_admin,
'is_adherent' => $is_adherent,
];
$_SESSION['id_token'] = $tokens['id_token'] ?? '';
$next = $_SESSION['next_url'] ?? '/profile.php';
unset($_SESSION['next_url']);
header('Location: ' . $next);
exit;
} catch (Exception $e) {
http_response_code(500);
exit('Erreur d\'authentification : ' . htmlspecialchars($e->getMessage()));
}

View file

@ -1,14 +0,0 @@
<?php
require_once __DIR__ . '/../inc/config.php';
require_once __DIR__ . '/../inc/auth.php';
require_once __DIR__ . '/../inc/oidc.php';
session_start_safe();
if (current_user()) {
header('Location: /dashboard.php');
exit;
}
header('Location: ' . oidc_authorization_url());
exit;

View file

@ -1,117 +0,0 @@
<?php
require_once __DIR__ . '/inc/config.php';
require_once __DIR__ . '/inc/auth.php';
require_once __DIR__ . '/inc/dolibarr.php';
require_once __DIR__ . '/inc/services.php';
session_start_safe();
require_login();
$user = current_user();
// Dolibarr est la source de vérité pour l'adhésion
$membership = null;
$doli_error = null;
try {
$membership = doli_get_membership($user['email']);
} catch (Exception $e) {
$doli_error = $e->getMessage();
}
// is_adherent = groupe KC OU cotisation active dans Dolibarr
$is_adherent = $user['is_adherent']
|| ($membership && $membership['status'] === 1 && $membership['date_fin_ts'] > time());
$services = services_list();
$title = 'Tableau de bord';
require __DIR__ . '/views/layout.php';
?>
<div class="dashboard">
<div class="dashboard-header">
<h1>Bonjour, <?= htmlspecialchars($user['name']) ?></h1>
<div class="badges">
<span class="badge badge-inscrit">Inscrit</span>
<?php if ($is_adherent): ?>
<span class="badge badge-adherent">Adhérent</span>
<?php endif; ?>
<?php if ($user['is_admin']): ?>
<span class="badge badge-admin">Admin</span>
<?php endif; ?>
</div>
</div>
<div class="dashboard-grid">
<!-- Profil -->
<div class="card">
<h2>Mon compte</h2>
<dl>
<dt>Identifiant</dt><dd><?= htmlspecialchars($user['username']) ?></dd>
<dt>Email</dt><dd><?= htmlspecialchars($user['email']) ?></dd>
<?php if ($user['groups']): ?>
<dt>Groupes</dt><dd><?= htmlspecialchars(implode(', ', $user['groups'])) ?></dd>
<?php endif; ?>
</dl>
<a href="/password.php" class="btn-outline btn-sm">Changer mon mot de passe</a>
</div>
<!-- Adhésion -->
<div class="card">
<h2>Mon adhésion</h2>
<?php if ($doli_error): ?>
<p class="text-warning">Impossible de contacter Dolibarr.</p>
<?php elseif ($membership): ?>
<dl>
<dt>Statut</dt>
<dd><?= $membership['status'] === 1
? '<span class="text-success">Actif</span>'
: '<span class="text-warning">Inactif</span>' ?></dd>
<?php if ($membership['date_fin']): ?>
<dt>Cotisation 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>
<a href="/helloasso/renew.php" class="btn-outline btn-sm">
Renouveler mon adhésion
</a>
<?php else: ?>
<p>Aucune adhésion trouvée.</p>
<p class="text-muted small">Adhérez pour accéder aux services réservés aux membres.</p>
<a href="<?= htmlspecialchars(HELLOASSO_URL) ?>" target="_blank" rel="noopener" class="btn-primary">
Adhérer via HelloAsso
</a>
<p class="text-muted small" style="margin-top:.8rem">Un administrateur validera votre adhésion sous 48h.</p>
<?php endif; ?>
</div>
</div>
<!-- Services -->
<section class="services-section">
<h2>Mes services</h2>
<div class="grid">
<?php foreach ($services as $s): if (!$s['visible']) continue;
$accessible = !$s['requires_adherent'] || $is_adherent;
?>
<div class="service-card <?= $accessible ? '' : 'locked' ?>">
<div class="service-header">
<strong><?= htmlspecialchars($s['name']) ?></strong>
<?php if ($s['requires_adherent']): ?>
<span class="badge"><?= $accessible ? 'Adhérent' : 'Adhérent requis' ?></span>
<?php endif; ?>
</div>
<p><?= htmlspecialchars($s['description']) ?></p>
<?php if ($accessible): ?>
<a href="<?= htmlspecialchars($s['url']) ?>" target="_blank" rel="noopener">Accéder</a>
<?php else: ?>
<span class="text-muted small">Adhésion requise</span>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
</section>
</div>
<?php require __DIR__ . '/views/layout_end.php'; ?>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

View file

@ -1,69 +0,0 @@
<?php
require_once __DIR__ . '/../inc/config.php';
require_once __DIR__ . '/../inc/auth.php';
require_once __DIR__ . '/../inc/helloasso.php';
require_once __DIR__ . '/../inc/keycloak.php';
require_once __DIR__ . '/../inc/dolibarr.php';
session_start_safe();
$checkout_id = $_GET['checkoutIntentId'] ?? '';
$kc_uid = $_GET['uid'] ?? '';
if (!$checkout_id || !$kc_uid) {
set_flash('error', 'Retour de paiement invalide.');
header('Location: /register.php');
exit;
}
try {
$checkout = ha_get_checkout($checkout_id);
$order = $checkout['order'] ?? null;
$payments = $order['payments'] ?? [];
$paid = false;
$amount = 0;
foreach ($payments as $p) {
if (($p['state'] ?? '') === 'Authorized') {
$paid = true;
$amount = (int)(($p['amount'] ?? HA_AMOUNT) / 100); // centimes → euros
break;
}
}
if (!$paid) {
set_flash('error', 'Paiement non confirmé. Veuillez réessayer ou contacter un administrateur.');
header('Location: /helloasso/renew.php');
exit;
}
// 1. Keycloak : ajouter au groupe adherents (idempotent)
kc_add_to_group($kc_uid, ADHERENT_GROUP);
// 2. Dolibarr : créer la cotisation (+1 an)
$kc_user = _kc_get_user($kc_uid);
$email = $kc_user['email'] ?? '';
$membership = $email ? doli_get_membership($email) : null;
if ($membership) {
doli_add_subscription($membership['id'], $membership['date_fin_ts'], $amount ?: HA_AMOUNT / 100);
}
// 3. TODO : Nextcloud — URL et credentials à configurer
// nc_add_to_group($email, 'adherents');
set_flash('success', 'Paiement confirmé ! Votre adhésion est activée.');
$next = current_user() ? '/dashboard.php' : '/auth/login.php';
header('Location: ' . $next);
exit;
} catch (Exception $e) {
set_flash('error', 'Erreur lors de la validation : ' . $e->getMessage());
header('Location: /helloasso/renew.php');
exit;
}
function _kc_get_user(string $uid): array {
$resp = _kc_request('GET', "/users/$uid");
return $resp['status'] === 200 ? json_decode($resp['body'], true) ?? [] : [];
}

View file

@ -1,92 +0,0 @@
<?php
require_once __DIR__ . '/../inc/config.php';
require_once __DIR__ . '/../inc/auth.php';
require_once __DIR__ . '/../inc/helloasso.php';
require_once __DIR__ . '/../inc/dolibarr.php';
session_start_safe();
require_login();
$user = current_user();
$membership = doli_get_membership($user['email']);
$first_name = $membership['firstname'] ?? $user['username'];
$last_name = $membership['lastname'] ?? $user['username'];
$address = $membership['address'] ?? '';
$zip = $membership['zip'] ?? '';
$town = $membership['town'] ?? '';
$country = $membership['country_code'] ?? 'FR';
$errors = [];
// Si l'adresse est complète, on part directement vers HelloAsso
if ($address && $zip && $town && $_SERVER['REQUEST_METHOD'] !== 'POST') {
_redirect_to_helloasso($first_name, $last_name, $user['email'], $user['sub'], $address, $zip, $town, $country);
}
// Traitement du formulaire d'adresse
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$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) {
_redirect_to_helloasso($first_name, $last_name, $user['email'], $user['sub'], $address, $zip, $town, $country);
}
}
function _redirect_to_helloasso(string $fn, string $ln, string $email, string $uid,
string $addr, string $zip, string $town, string $country): void {
try {
$checkout = ha_create_checkout($fn, $ln, $email, $uid, $addr, $zip, $town, $country);
header('Location: ' . $checkout['redirectUrl']);
exit;
} catch (Exception $e) {
set_flash('error', 'Impossible de créer le lien de paiement : ' . $e->getMessage());
header('Location: /dashboard.php');
exit;
}
}
$title = 'Renouveler mon adhésion';
require __DIR__ . '/../views/layout.php';
?>
<div class="card">
<h1>Renouveler mon adhésion</h1>
<p class="subtitle">Votre adresse postale est nécessaire pour finaliser l'adhésion.</p>
<?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>
<div class="form-group">
<label for="address">Adresse</label>
<input type="text" id="address" name="address" required
placeholder="1 rue de la Paix"
value="<?= htmlspecialchars($address) ?>">
</div>
<div class="form-row">
<div class="form-group">
<label for="zip">Code postal</label>
<input type="text" id="zip" name="zip" required maxlength="10"
value="<?= htmlspecialchars($zip) ?>">
</div>
<div class="form-group">
<label for="town">Ville</label>
<input type="text" id="town" name="town" required
value="<?= htmlspecialchars($town) ?>">
</div>
</div>
<button type="submit" class="btn-primary btn-full">Continuer vers le paiement</button>
</form>
</div>
<?php require __DIR__ . '/../views/layout_end.php'; ?>

View file

@ -1,202 +0,0 @@
<?php
require_once __DIR__ . '/inc/config.php';
require_once __DIR__ . '/inc/auth.php';
require_once __DIR__ . '/inc/keycloak.php';
session_start_safe();
require_login();
$user = current_user();
// Récupérer les events LOGIN depuis Keycloak Admin API
function kc_events(string $user_id, string $type = 'LOGIN', int $max = 500): array {
$resp = _kc_request('GET', '/events?user=' . urlencode($user_id) . '&type=' . $type . '&max=' . $max);
return $resp['status'] === 200 ? (json_decode($resp['body'], true) ?? []) : [];
}
$logins = kc_events($user['sub'], 'LOGIN', 500);
$errors = kc_events($user['sub'], 'LOGIN_ERROR', 200);
$logouts = kc_events($user['sub'], 'LOGOUT', 200);
// ── Calculs stats ─────────────────────────────────────────────────────
$now = time() * 1000;
$ms_30d = 30 * 86400 * 1000;
$logins_30d = array_filter($logins, fn($e) => $e['time'] >= ($now - $ms_30d));
$errors_30d = array_filter($errors, fn($e) => $e['time'] >= ($now - $ms_30d));
// Logins par jour (30 derniers jours)
$by_day = [];
for ($i = 29; $i >= 0; $i--) {
$d = date('d/m', strtotime("-$i days"));
$by_day[$d] = 0;
}
foreach ($logins_30d as $e) {
$d = date('d/m', (int)($e['time'] / 1000));
if (isset($by_day[$d])) $by_day[$d]++;
}
// Logins par heure (0-23)
$by_hour = array_fill(0, 24, 0);
foreach ($logins as $e) {
$h = (int)date('G', (int)($e['time'] / 1000));
$by_hour[$h]++;
}
// Logins par jour de semaine (lundi = 0)
$days_fr = ['Lun','Mar','Mer','Jeu','Ven','Sam','Dim'];
$by_dow = array_fill(0, 7, 0);
foreach ($logins as $e) {
$by_dow[((int)date('w', (int)($e['time'] / 1000)) + 6) % 7]++;
}
// IPs uniques
$ips = array_count_values(array_column($logins, 'ipAddress'));
arsort($ips);
// Dernière connexion
$last_login = $logins ? max(array_column($logins, 'time')) : null;
$title = 'Historique des connexions';
require __DIR__ . '/views/layout.php';
?>
<div class="history-page">
<div class="page-header">
<h1>Historique des connexions</h1>
<a href="/profile.php" class="btn-outline btn-sm">Retour au profil</a>
</div>
<!-- Chiffres clés -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value"><?= count($logins) ?></div>
<div class="stat-label">Connexions totales</div>
</div>
<div class="stat-card">
<div class="stat-value"><?= count($logins_30d) ?></div>
<div class="stat-label">30 derniers jours</div>
</div>
<div class="stat-card">
<div class="stat-value"><?= count($ips) ?></div>
<div class="stat-label">IP distinctes</div>
</div>
<div class="stat-card <?= count($errors_30d) > 0 ? 'stat-warning' : '' ?>">
<div class="stat-value"><?= count($errors_30d) ?></div>
<div class="stat-label">Échecs (30j)</div>
</div>
<div class="stat-card">
<div class="stat-value"><?= count($logouts) ?></div>
<div class="stat-label">Déconnexions</div>
</div>
<div class="stat-card">
<div class="stat-value"><?= $last_login ? date('d/m/Y', (int)($last_login/1000)) : '—' ?></div>
<div class="stat-label">Dernière connexion</div>
</div>
</div>
<!-- Graphiques -->
<div class="charts-grid">
<div class="card chart-card">
<h2>Connexions par jour (30 derniers jours)</h2>
<canvas id="chartDay"></canvas>
</div>
<div class="card chart-card">
<h2>Par heure de la journée</h2>
<canvas id="chartHour"></canvas>
</div>
<div class="card chart-card">
<h2>Par jour de la semaine</h2>
<canvas id="chartDow"></canvas>
</div>
<div class="card chart-card">
<h2>IPs les plus fréquentes</h2>
<?php if ($ips): ?>
<div class="ip-bars">
<?php $max_ip = max(array_values($ips)); foreach (array_slice($ips, 0, 8, true) as $ip => $count): ?>
<div class="ip-bar-row">
<span class="ip-label"><?= htmlspecialchars($ip) ?></span>
<div class="ip-bar-wrap">
<div class="ip-bar" style="width:<?= round($count/$max_ip*100) ?>%"></div>
</div>
<span class="ip-count"><?= $count ?></span>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<p class="text-muted">Aucune donnée.</p>
<?php endif; ?>
</div>
</div>
<!-- Historique récent -->
<div class="card" style="max-width:none;margin-top:1.5rem">
<h2>Dernières connexions</h2>
<?php $recent = array_slice($logins, 0, 30); ?>
<?php if ($recent): ?>
<table class="members-table">
<thead>
<tr>
<th>Date</th>
<th>Heure</th>
<th>Adresse IP</th>
</tr>
</thead>
<tbody>
<?php foreach ($recent as $e): $ts = (int)($e['time']/1000); ?>
<tr>
<td><?= date('d/m/Y', $ts) ?></td>
<td><?= date('H:i:s', $ts) ?></td>
<td><code><?= htmlspecialchars($e['ipAddress'] ?? '—') ?></code></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p class="text-muted">Aucune connexion enregistrée.</p>
<?php endif; ?>
</div>
<?php if ($errors): ?>
<div class="card" style="max-width:none;margin-top:1rem">
<h2 class="text-warning">Tentatives de connexion échouées</h2>
<table class="members-table">
<thead><tr><th>Date</th><th>Heure</th><th>IP</th></tr></thead>
<tbody>
<?php foreach (array_slice($errors, 0, 20) as $e): $ts=(int)($e['time']/1000); ?>
<tr>
<td><?= date('d/m/Y', $ts) ?></td>
<td><?= date('H:i:s', $ts) ?></td>
<td><code><?= htmlspecialchars($e['ipAddress'] ?? '—') ?></code></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
<script>
const primary = '#3b5bdb', light = '#dde4ff', muted = '#6b7289';
const opts = { responsive: true, plugins: { legend: { display: false } },
scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } } } };
new Chart(document.getElementById('chartDay'), { type: 'bar', options: opts, data: {
labels: <?= json_encode(array_keys($by_day)) ?>,
datasets: [{ data: <?= json_encode(array_values($by_day)) ?>, backgroundColor: primary }]
}});
new Chart(document.getElementById('chartHour'), { type: 'bar', options: opts, data: {
labels: <?= json_encode(array_map(fn($h) => str_pad($h,2,'0',STR_PAD_LEFT).'h', range(0,23))) ?>,
datasets: [{ data: <?= json_encode($by_hour) ?>, backgroundColor: primary }]
}});
new Chart(document.getElementById('chartDow'), { type: 'bar', options: opts, data: {
labels: <?= json_encode(array_values($days_fr)) ?>,
datasets: [{ data: <?= json_encode(array_values($by_dow)) ?>, backgroundColor: primary }]
}});
</script>
<?php require __DIR__ . '/views/layout_end.php'; ?>

View file

@ -1 +0,0 @@
Require all denied

View file

@ -1,58 +0,0 @@
<?php
require_once __DIR__ . '/config.php';
function session_start_safe(): void {
if (session_status() === PHP_SESSION_NONE) {
session_name('portail_sess');
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
}
}
function current_user(): ?array {
session_start_safe();
return $_SESSION['user'] ?? null;
}
function require_login(): void {
$user = current_user();
if (!$user) {
$_SESSION['next_url'] = (isset($_SERVER['HTTPS']) ? 'https' : 'http')
. '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
header('Location: /auth/login.php');
exit;
}
}
function require_admin(): void {
require_login();
$user = current_user();
if (!($user['is_admin'] ?? false)) {
http_response_code(403);
require __DIR__ . '/../views/403.php';
exit;
}
}
function is_adherent(): bool {
$user = current_user();
return $user ? (bool)($user['is_adherent'] ?? false) : false;
}
function set_flash(string $type, string $message): void {
session_start_safe();
$_SESSION['flash'] = ['type' => $type, 'message' => $message];
}
function get_flash(): ?array {
session_start_safe();
$flash = $_SESSION['flash'] ?? null;
unset($_SESSION['flash']);
return $flash;
}

View file

@ -1,54 +0,0 @@
<?php
// Charge .env depuis private/ (dans open_basedir, hors web root public)
$_env_file = dirname(__DIR__) . '/../private/.env';
if (is_file($_env_file)) {
foreach (file($_env_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
if ($line[0] === '#' || !str_contains($line, '=')) continue;
[$k, $v] = explode('=', $line, 2);
$_ENV[trim($k)] = trim($v);
putenv(trim($k) . '=' . trim($v));
}
}
function env(string $key, string $default = ''): string {
return $_ENV[$key] ?? getenv($key) ?: $default;
}
// ── Secrets ───────────────────────────────────────────────────────────
define('SECRET_KEY', env('SECRET_KEY'));
define('ALPID_CLIENT_ID', env('ALPID_CLIENT_ID'));
define('ALPID_CLIENT_SECRET',env('ALPID_CLIENT_SECRET'));
// ── AlpID / Keycloak OIDC (realm alpinux) ─────────────────────────────
define('ALPID_BASE', rtrim(env('ALPID_BASE', 'https://alpid.alpinux.org'), '/'));
define('ALPID_REALM', env('ALPID_REALM', 'alpinux'));
define('ALPID_AUTH_URL', ALPID_BASE . '/realms/' . ALPID_REALM . '/protocol/openid-connect/auth');
define('ALPID_TOKEN_URL', ALPID_BASE . '/realms/' . ALPID_REALM . '/protocol/openid-connect/token');
define('ALPID_USERINFO_URL', ALPID_BASE . '/realms/' . ALPID_REALM . '/protocol/openid-connect/userinfo');
define('ALPID_LOGOUT_URL', ALPID_BASE . '/realms/' . ALPID_REALM . '/protocol/openid-connect/logout');
// ── Keycloak Admin REST API — compte de service (realm master) ────────
define('KC_SERVICE_CLIENT_ID', env('KC_SERVICE_CLIENT_ID', 'portail-service'));
define('KC_SERVICE_CLIENT_SECRET', env('KC_SERVICE_CLIENT_SECRET'));
define('KC_ADMIN_BASE', ALPID_BASE . '/admin/realms/' . ALPID_REALM);
// ── Dolibarr ──────────────────────────────────────────────────────────
define('DOLIBARR_URL', rtrim(env('DOLIBARR_URL', 'https://dolibarr.alpinux.org'), '/'));
define('DOLIBARR_API_KEY', env('DOLIBARR_API_KEY'));
// ── Application ───────────────────────────────────────────────────────
define('APP_URL', rtrim(env('APP_URL', 'https://portail.alpinux.org'), '/'));
define('CALLBACK_URL', APP_URL . '/auth/callback.php');
define('HELLOASSO_URL', env('HELLOASSO_URL',
'https://www.helloasso.com/associations/alpinux-le-lug-de-savoie/adhesions'));
define('ADMIN_GROUPS', array_filter(explode(',', env('ADMIN_GROUPS', 'admins'))));
define('ADHERENT_GROUP', env('ADHERENT_GROUP', 'adherents'));
define('SERVICES_FILE', env('SERVICES_FILE', dirname(__DIR__, 2) . '/services.json'));
// ── HelloAsso API ──────────────────────────────────────────────────────
define('HA_CLIENT_ID', env('HA_CLIENT_ID'));
define('HA_CLIENT_SECRET', env('HA_CLIENT_SECRET'));
define('HA_ORG_SLUG', env('HA_ORG_SLUG', 'alpinux-le-lug-de-savoie'));
define('HA_FORM_SLUG', env('HA_FORM_SLUG', 'adhesions-a-l-annee'));
define('HA_AMOUNT', (int)env('HA_AMOUNT', '1500'));
define('HA_ITEM_NAME', env('HA_ITEM_NAME', 'Adhésion Alpinux'));

View file

@ -1,94 +0,0 @@
<?php
require_once __DIR__ . '/config.php';
function _doli_get(string $path, array $params = []): array {
$url = DOLIBARR_URL . '/api/index.php/' . ltrim($path, '/');
if ($params) $url .= '?' . http_build_query($params);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ['DOLAPIKEY: ' . DOLIBARR_API_KEY, 'Accept: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return ['status' => $status, 'data' => json_decode($body, true)];
}
function doli_update_member(string $member_id, array $fields): void {
$url = DOLIBARR_URL . '/api/index.php/members/' . $member_id;
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_POSTFIELDS => json_encode($fields),
CURLOPT_HTTPHEADER => ['DOLAPIKEY: ' . DOLIBARR_API_KEY, 'Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) throw new RuntimeException("Dolibarr update échoué ($status)");
}
function doli_add_subscription(string $member_id, int $date_fin_actuel, int $amount): void {
$now = time();
$start = ($date_fin_actuel > $now) ? $date_fin_actuel : $now;
$end = strtotime('+1 year', $start);
$resp = _doli_post("members/$member_id/subscriptions", [
'start_date' => $start,
'end_date' => $end,
'amount' => $amount,
'label' => 'Adhésion annuelle (HelloAsso)',
]);
if ($resp['status'] !== 200 && $resp['status'] !== 201) {
throw new RuntimeException("Dolibarr subscription échouée ({$resp['status']}): {$resp['body']}");
}
}
function _doli_post(string $path, array $data): array {
$url = DOLIBARR_URL . '/api/index.php/' . ltrim($path, '/');
$body = json_encode($data);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => ['DOLAPIKEY: ' . DOLIBARR_API_KEY, 'Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$resp = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return ['status' => $status, 'body' => $resp];
}
function doli_get_membership(string $email): ?array {
$resp = _doli_get('members', ['sqlfilters' => "(t.email:=:'$email')"]);
if ($resp['status'] !== 200 || empty($resp['data'])) return null;
$m = $resp['data'][0];
$date_fin = $m['datefin'] ?? null;
return [
'id' => $m['id'] ?? null,
'firstname' => $m['firstname'] ?? '',
'lastname' => $m['lastname'] ?? '',
'email' => $m['email'] ?? '',
'status' => (int)($m['status'] ?? 0),
'date_fin' => $date_fin ? date('d/m/Y', (int)$date_fin) : null,
'date_fin_ts' => $date_fin ? (int)$date_fin : 0,
'type_label' => $m['type'] ?? '',
'address' => $m['address'] ?? null,
'zip' => $m['zip'] ?? null,
'town' => $m['town'] ?? null,
'country_code' => $m['country_code'] ?? 'FR',
];
}

View file

@ -1,95 +0,0 @@
<?php
require_once __DIR__ . '/config.php';
function _ha_token(): string {
static $cache = null;
if ($cache && $cache['expires'] > time()) return $cache['token'];
$ch = curl_init('https://api.helloasso.com/oauth2/token');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query([
'grant_type' => 'client_credentials',
'client_id' => HA_CLIENT_ID,
'client_secret' => HA_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("HelloAsso auth échouée ($status): $body");
}
$data = json_decode($body, true);
$cache = ['token' => $data['access_token'], 'expires' => time() + $data['expires_in'] - 30];
return $cache['token'];
}
// ISO 3166-1 alpha-2 → alpha-3 (HelloAsso exige alpha-3)
function _ha_country(string $alpha2): string {
$map = ['FR'=>'FRA','BE'=>'BEL','CH'=>'CHE','LU'=>'LUX','DE'=>'DEU',
'ES'=>'ESP','IT'=>'ITA','GB'=>'GBR','NL'=>'NLD','PT'=>'PRT'];
return $map[strtoupper($alpha2)] ?? 'FRA';
}
function ha_create_checkout(string $first_name, string $last_name, string $email, string $kc_uid,
string $address = '', string $zip = '', string $town = '', string $country = 'FR'): array {
$token = _ha_token();
$return_url = APP_URL . '/helloasso/callback.php?uid=' . urlencode($kc_uid);
$back_url = APP_URL . '/helloasso/renew.php';
$payer = ['firstName' => $first_name, 'lastName' => $last_name, 'email' => $email];
if ($address) $payer['address'] = $address;
if ($zip) $payer['zipCode'] = $zip;
if ($town) $payer['city'] = $town;
if ($country) $payer['country'] = _ha_country($country);
$ch = curl_init('https://api.helloasso.com/v5/organizations/' . HA_ORG_SLUG . '/checkout-intents');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'totalAmount' => HA_AMOUNT,
'initialAmount' => HA_AMOUNT,
'itemName' => HA_ITEM_NAME,
'backUrl' => $back_url,
'errorUrl' => $back_url,
'returnUrl' => $return_url,
'containsDonation' => false,
'payer' => $payer,
]),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200 && $status !== 201) {
throw new RuntimeException("HelloAsso checkout échoué ($status): $body");
}
return json_decode($body, true);
}
function ha_get_checkout(string $checkout_intent_id): array {
$token = _ha_token();
$ch = curl_init("https://api.helloasso.com/v5/checkout-intents/$checkout_intent_id");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token"],
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("HelloAsso get checkout échoué ($status): $body");
}
return json_decode($body, true);
}

View file

@ -1,200 +0,0 @@
<?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']})");
}
}

View file

@ -1,62 +0,0 @@
<?php
require_once __DIR__ . '/config.php';
function oidc_authorization_url(): string {
$state = bin2hex(random_bytes(16));
$nonce = bin2hex(random_bytes(16));
$_SESSION['oidc_state'] = $state;
$_SESSION['oidc_nonce'] = $nonce;
return ALPID_AUTH_URL . '?' . http_build_query([
'client_id' => ALPID_CLIENT_ID,
'response_type' => 'code',
'scope' => 'openid profile email',
'redirect_uri' => CALLBACK_URL,
'state' => $state,
'nonce' => $nonce,
]);
}
function oidc_exchange_code(string $code): array {
$ch = curl_init(ALPID_TOKEN_URL);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query([
'grant_type' => 'authorization_code',
'code' => $code,
'redirect_uri' => CALLBACK_URL,
'client_id' => ALPID_CLIENT_ID,
'client_secret' => ALPID_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("Erreur token OIDC ($status) : $body");
}
return json_decode($body, true);
}
function oidc_userinfo(string $access_token): array {
$ch = curl_init(ALPID_USERINFO_URL);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["Authorization: Bearer $access_token"],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$body = curl_exec($ch);
curl_close($ch);
return json_decode($body, true) ?? [];
}
function oidc_logout_url(string $id_token = ''): string {
$params = ['post_logout_redirect_uri' => APP_URL . '/'];
if ($id_token) {
$params['id_token_hint'] = $id_token;
}
return ALPID_LOGOUT_URL . '?' . http_build_query($params);
}

View file

@ -1,26 +0,0 @@
<?php
require_once __DIR__ . '/config.php';
const DEFAULT_SERVICES = [
['name' => 'Wiki', 'url' => 'https://wiki.alpinux.org', 'description' => 'Documentation et guides', 'requires_adherent' => false, 'visible' => true],
['name' => 'Gitea', 'url' => 'https://gitea.alpinux.org', 'description' => 'Forge de code', 'requires_adherent' => false, 'visible' => true],
['name' => 'Quiz interactifs','url' => 'https://dynamic.alpinux.org', 'description' => 'Jeux et quiz', 'requires_adherent' => false, 'visible' => true],
['name' => 'Install Party', 'url' => 'https://installparty.alpinux.org', 'description' => 'Événements et ateliers', 'requires_adherent' => false, 'visible' => true],
['name' => 'Nextcloud', 'url' => 'https://cloud.alpinux.org', 'description' => 'Stockage et collaboration', 'requires_adherent' => true, 'visible' => true],
['name' => 'Dolibarr', 'url' => 'https://dolibarr.alpinux.org', 'description' => 'Gestion de l\'association', 'requires_adherent' => true, 'visible' => true],
];
function services_list(): array {
$file = SERVICES_FILE;
if (is_file($file)) {
$data = json_decode(file_get_contents($file), true);
if (is_array($data)) return $data;
}
return DEFAULT_SERVICES;
}
function services_save(array $services): void {
$dir = dirname(SERVICES_FILE);
if (!is_dir($dir)) mkdir($dir, 0755, true);
file_put_contents(SERVICES_FILE, json_encode($services, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
}

View file

@ -1,98 +0,0 @@
<?php
require_once __DIR__ . '/inc/config.php';
require_once __DIR__ . '/inc/auth.php';
require_once __DIR__ . '/inc/services.php';
session_start_safe();
$user = current_user();
$title = 'Portail Alpinux';
$services = services_list();
$is_adherent = $user && (
$user['is_adherent'] ||
($user['is_admin'] ?? false)
);
require __DIR__ . '/views/layout.php';
?>
<?php if ($user): ?>
<div class="hero">
<h1>Bienvenue, <?= htmlspecialchars($user['name'] ?: $user['username']) ?></h1>
<?php if (!$is_adherent): ?>
<p>Vous n'avez pas encore adhéré à Alpinux. Adhérer, c'est soutenir nos actions pour le logiciel libre en Savoie.</p>
<div class="hero-actions">
<a href="/helloasso/renew.php" class="btn-primary">Adhérer maintenant</a>
<a href="/profile.php" class="btn-outline">Mon profil</a>
</div>
<?php else: ?>
<p>Merci pour votre soutien. Retrouvez vos services et votre profil ci-dessous.</p>
<div class="hero-actions">
<a href="/profile.php" class="btn-outline">Mon profil</a>
</div>
<?php endif; ?>
</div>
<?php else: ?>
<div class="hero">
<h1>Portail membres Alpinux</h1>
<p>Accédez à vos services, gérez votre adhésion et votre compte AlpID.</p>
<div class="hero-actions">
<a href="/register.php" class="btn-primary">Créer mon compte</a>
<a href="/auth/login.php" class="btn-outline">Se connecter</a>
</div>
</div>
<section class="home-about">
<div class="about-grid">
<div class="about-card">
<div class="about-title">Qui sommes-nous ?</div>
<p>Alpinux est le LUG (Linux User Group) de Savoie. Nous promouvons le logiciel libre, organisons des ateliers et des événements autour de Linux et des technologies ouvertes.</p>
</div>
<div class="about-card">
<div class="about-title">Pourquoi adhérer ?</div>
<p>Adhérer, c'est soutenir nos actions pour le logiciel libre en Savoie. C'est aussi accéder à l'ensemble des services membres et participer aux décisions lors de l'assemblée générale.</p>
</div>
<div class="about-card">
<div class="about-title">AlpID</div>
<p>Un seul compte pour tous les services Alpinux. Gérez votre profil, votre adresse et votre adhésion depuis ce portail.</p>
</div>
</div>
</section>
<?php endif; ?>
<section class="services-section">
<h2>Nos services</h2>
<div class="grid">
<?php foreach ($services as $s): if (!$s['visible']) continue;
$can_access = !$s['requires_adherent'] || $is_adherent; ?>
<div class="service-card <?= $s['requires_adherent'] ? 'adherent-only' : '' ?> <?= (!$can_access) ? 'locked' : '' ?>">
<div class="service-header">
<strong><?= htmlspecialchars($s['name']) ?></strong>
<?php if ($s['requires_adherent']): ?>
<span class="badge">Adhérent</span>
<?php endif; ?>
</div>
<?php if ($s['description']): ?>
<p><?= htmlspecialchars($s['description']) ?></p>
<?php endif; ?>
<div style="margin-top:.8rem">
<?php if ($can_access): ?>
<a href="<?= htmlspecialchars($s['url']) ?>" target="_blank" rel="noopener" style="font-size:.85rem">Accéder </a>
<?php elseif ($user): ?>
<span class="text-muted small">Adhésion requise</span>
<?php else: ?>
<span class="text-muted small">Réservé aux adhérents </span>
<a href="/register.php" style="font-size:.85rem">Adhérer</a>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
</section>
<?php require __DIR__ . '/views/layout_end.php'; ?>

View file

@ -1,11 +0,0 @@
<?php
require_once __DIR__ . '/inc/config.php';
require_once __DIR__ . '/inc/auth.php';
require_once __DIR__ . '/inc/oidc.php';
session_start_safe();
$id_token = $_SESSION['id_token'] ?? '';
session_destroy();
header('Location: ' . oidc_logout_url($id_token));
exit;

View file

@ -1,66 +0,0 @@
<?php
require_once __DIR__ . '/inc/config.php';
require_once __DIR__ . '/inc/auth.php';
require_once __DIR__ . '/inc/keycloak.php';
session_start_safe();
require_login();
$user = current_user();
$errors = [];
$success = false;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$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);
$success = true;
} catch (Exception $e) {
$errors[] = 'Erreur : ' . $e->getMessage();
}
}
}
$title = 'Changer mon mot de passe';
require __DIR__ . '/views/layout.php';
?>
<div class="card">
<h1>Changer mon mot de passe</h1>
<?php if ($success): ?>
<div class="alert alert-success">Mot de passe modifié avec succès.</div>
<?php endif; ?>
<?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>
<div class="form-group">
<label for="password">Nouveau mot de passe</label>
<input type="password" id="password" name="password" required minlength="8" autocomplete="new-password">
</div>
<div class="form-group">
<label for="password2">Confirmer le nouveau mot de passe</label>
<input type="password" id="password2" name="password2" required autocomplete="new-password">
</div>
<button type="submit" class="btn-primary">Enregistrer</button>
<a href="/dashboard.php" class="btn-outline">Annuler</a>
</form>
</div>
<?php require __DIR__ . '/views/layout_end.php'; ?>

View file

@ -1,385 +0,0 @@
<?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']);
$otp_setup_url = ALPID_BASE . '/realms/' . ALPID_REALM . '/account/#/security/signingin';
$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'] ?>&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="<?= htmlspecialchars($otp_setup_url) ?>" target="_blank" rel="noopener"
class="btn-outline 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'; ?>

View file

@ -1,45 +0,0 @@
<?php
require_once __DIR__ . '/inc/config.php';
require_once __DIR__ . '/inc/auth.php';
session_start_safe();
$title = 'Inscription réussie';
require __DIR__ . '/views/layout.php';
?>
<div class="card center">
<div class="success-icon"></div>
<h1>Compte créé avec succès !</h1>
<p>Votre compte AlpID est prêt. Vous pouvez maintenant vous connecter.</p>
<div class="steps">
<div class="step done">
<span class="step-num">1</span>
<div>
<strong>Compte AlpID créé</strong>
<p>Votre identifiant pour tous les services Alpinux.</p>
</div>
</div>
<div class="step">
<span class="step-num">2</span>
<div>
<strong>Adhérer à l'association</strong>
<p>Payez votre cotisation sur HelloAsso pour accéder à tous les services.</p>
<a href="<?= htmlspecialchars(HELLOASSO_URL) ?>" target="_blank" rel="noopener" class="btn-primary">
Adhérer sur HelloAsso
</a>
</div>
</div>
<div class="step">
<span class="step-num">3</span>
<div>
<strong>Validation par l'équipe</strong>
<p>Un administrateur validera votre adhésion sous 48h.</p>
</div>
</div>
</div>
<a href="/auth/login.php" class="btn-outline">Se connecter maintenant</a>
</div>
<?php require __DIR__ . '/views/layout_end.php'; ?>

View file

@ -1,135 +0,0 @@
<?php
require_once __DIR__ . '/inc/config.php';
require_once __DIR__ . '/inc/auth.php';
require_once __DIR__ . '/inc/keycloak.php';
session_start_safe();
if (current_user()) {
header('Location: /profile.php');
exit;
}
$errors = [];
$form = ['email' => '', 'first_name' => '', 'last_name' => ''];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$form = [
'email' => trim($_POST['email'] ?? ''),
'first_name' => trim($_POST['first_name'] ?? ''),
'last_name' => trim($_POST['last_name'] ?? ''),
];
$password = $_POST['password'] ?? '';
$password2 = $_POST['password2'] ?? '';
if (!$form['email'] || !$form['first_name'] || !$form['last_name'] || !$password) {
$errors[] = 'Tous les champs sont obligatoires.';
}
if ($password !== $password2) {
$errors[] = 'Les mots de passe ne correspondent pas.';
}
if (strlen($password) < 8) {
$errors[] = 'Le mot de passe doit contenir au moins 8 caractères.';
}
if (!filter_var($form['email'], FILTER_VALIDATE_EMAIL)) {
$errors[] = 'Adresse email invalide.';
}
if (!$errors) {
try {
$username = kc_generate_username($form['last_name'], $form['first_name']);
kc_create_user($username, $form['email'], $form['first_name'], $form['last_name'], $password);
set_flash('success', "Compte créé. Votre identifiant AlpID est : $username");
header('Location: /auth/login.php');
exit;
} catch (KcUserExistsException $e) {
$errors[] = $e->getMessage();
} catch (Exception $e) {
$errors[] = 'Erreur : ' . $e->getMessage();
}
}
}
$title = 'Inscription';
require __DIR__ . '/views/layout.php';
?>
<div class="card">
<h1>Créer mon compte AlpID</h1>
<p class="subtitle">Créez votre compte pour accéder aux services Alpinux. L'adhésion se fait ensuite volontairement depuis votre profil.</p>
<?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>
<div class="form-row">
<div class="form-group">
<label for="first_name">Prénom</label>
<input type="text" id="first_name" name="first_name" required autofocus
value="<?= htmlspecialchars($form['first_name']) ?>">
</div>
<div class="form-group">
<label for="last_name">Nom</label>
<input type="text" id="last_name" name="last_name" required
value="<?= htmlspecialchars($form['last_name']) ?>">
</div>
</div>
<div id="username-hint" class="username-hint" style="display:none">
Votre identifiant AlpID sera : <strong id="username-preview"></strong>
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" required autocomplete="email"
value="<?= htmlspecialchars($form['email']) ?>">
</div>
<div class="form-row">
<div class="form-group">
<label for="password">Mot de passe</label>
<input type="password" id="password" name="password" required autocomplete="new-password" minlength="8">
</div>
<div class="form-group">
<label for="password2">Confirmer</label>
<input type="password" id="password2" name="password2" required autocomplete="new-password">
</div>
</div>
<button type="submit" class="btn-primary btn-full">Créer mon compte</button>
</form>
<p class="form-footer">Déjà inscrit ? <a href="/auth/login.php">Se connecter</a></p>
</div>
<script>
function normalizeStr(s) {
return s.normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-zA-Z]/g, '');
}
function encodeLastname(ln) {
const n = normalizeStr(ln).toLowerCase();
if (n.length <= 2) return n;
return n[0] + (n.length - 2) + n[n.length - 1];
}
function buildUsername(fn, ln) {
const first = normalizeStr(fn).toLowerCase();
const code = encodeLastname(ln);
if (!first && !code) return '';
if (!first) return code;
if (!code) return first;
return first + '.' + code;
}
function updatePreview() {
const u = buildUsername(
document.getElementById('first_name').value.trim(),
document.getElementById('last_name').value.trim()
);
const hint = document.getElementById('username-hint');
if (!u) { hint.style.display = 'none'; return; }
document.getElementById('username-preview').textContent = u;
hint.style.display = 'block';
}
document.getElementById('last_name').addEventListener('input', updatePreview);
document.getElementById('first_name').addEventListener('input', updatePreview);
</script>
<?php require __DIR__ . '/views/layout_end.php'; ?>

View file

@ -1,5 +0,0 @@
User-agent: *
Disallow: /admin/
Disallow: /auth/
Disallow: /inc/
Disallow: /views/

View file

@ -1 +0,0 @@
Require all denied

View file

@ -1,7 +0,0 @@
<?php $title = 'Accès refusé'; require __DIR__ . '/layout.php'; ?>
<div class="card center">
<h1>403 Accès refusé</h1>
<p>Vous n'avez pas les droits nécessaires pour accéder à cette page.</p>
<a href="/" class="btn-primary">Retour à l'accueil</a>
</div>
<?php require __DIR__ . '/layout_end.php'; ?>

View file

@ -1,38 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= htmlspecialchars($title ?? 'Portail Alpinux') ?></title>
<link rel="stylesheet" href="/assets/style.css">
</head>
<body>
<header>
<nav>
<a href="/" class="brand">
<img src="https://static.alpinux.org/logo/alpinux-logo.svg" alt="Alpinux" height="32">
<span>Portail membres</span>
</a>
<div class="nav-links">
<?php if ($user = current_user()): ?>
<a href="/profile.php">Mon profil</a>
<?php if ($user['is_admin']): ?>
<a href="/admin/members.php">Admin</a>
<?php endif; ?>
<a href="/logout.php" class="btn-outline">Déconnexion</a>
<?php else: ?>
<a href="/register.php">S'inscrire</a>
<a href="/auth/login.php" class="btn-primary">Connexion</a>
<?php endif; ?>
</div>
</nav>
</header>
<main>
<?php
$flash = get_flash();
if ($flash): ?>
<div class="flash flash-<?= htmlspecialchars($flash['type']) ?>">
<?= htmlspecialchars($flash['message']) ?>
</div>
<?php endif; ?>

View file

@ -1,11 +0,0 @@
</main>
<footer>
<p>
<a href="https://alpinux.org">alpinux.org</a> &nbsp;·&nbsp;
<a href="https://wiki.alpinux.org">Wiki</a> &nbsp;·&nbsp;
<a href="<?= htmlspecialchars(HELLOASSO_URL) ?>">Adhérer via HelloAsso</a>
</p>
</footer>
</body>
</html>