feat: admin panel + page paramètres + README mis à jour
Deploy to Pacha Family / deploy (push) Failing after 3s
Deploy to Pacha Family / deploy (push) Failing after 3s
- admin/index.php : gestion familles et utilisateurs (activer/désactiver, rôles, supprimer, regénérer code invite) - includes/admin_auth.php : protection des routes admin (403 si non-admin) - settings.php : page de config par compte (profil, mot de passe, code invitation, membres, lien admin) - login.php : vérification is_active user + famille, is_admin en session - docker/init/00-setup.sql : ajout colonnes is_admin, is_active sur users + is_active sur families - invite.php supprimé (remplacé par settings.php) - README.md : documentation complète multi-tenant, URLs, rôle admin Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
84db4b6619
commit
0494554c30
@@ -20,9 +20,32 @@ docker compose up -d --build
|
|||||||
# https://househub.nas.percolouco.com
|
# https://househub.nas.percolouco.com
|
||||||
```
|
```
|
||||||
|
|
||||||
La base de données est **initialisée automatiquement** depuis `schema.sql` au premier démarrage.
|
La base de données meta (`househub_meta`) et les permissions sont **initialisées automatiquement** au premier démarrage via `docker/init/00-setup.sql`.
|
||||||
|
|
||||||
Compte par défaut : `admin` / `password` → **à changer immédiatement** dans la table `pf_users`.
|
Aucun compte par défaut — le premier utilisateur s'inscrit via `/register.php` et choisit de créer un espace familial.
|
||||||
|
|
||||||
|
## 👥 Gestion multi-tenant
|
||||||
|
|
||||||
|
HouseHub supporte plusieurs familles indépendantes sur la même instance :
|
||||||
|
|
||||||
|
| URL | Description |
|
||||||
|
|-----|-------------|
|
||||||
|
| `/register.php` | Créer un compte + nouvel espace, ou rejoindre un espace existant via code |
|
||||||
|
| `/login.php` | Connexion |
|
||||||
|
| `/settings.php` | Paramètres du compte : profil, mot de passe, code d'invitation, membres |
|
||||||
|
| `/admin/` | Panneau d'administration (admin uniquement) |
|
||||||
|
|
||||||
|
### Inviter quelqu'un dans son espace
|
||||||
|
1. Aller sur `/settings.php` → copier le code d'invitation
|
||||||
|
2. La personne va sur `/register.php` → onglet "Rejoindre" → coller le code
|
||||||
|
3. Les deux comptes partagent le même environnement de données
|
||||||
|
|
||||||
|
### Rôle admin
|
||||||
|
Le premier utilisateur inscrit doit être promu admin manuellement :
|
||||||
|
```sql
|
||||||
|
UPDATE househub_meta.users SET is_admin = 1 WHERE username = 'ton_username';
|
||||||
|
```
|
||||||
|
L'admin peut ensuite promouvoir/désactiver d'autres utilisateurs depuis `/admin/`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+196
@@ -0,0 +1,196 @@
|
|||||||
|
<?php
|
||||||
|
require __DIR__ . '/../includes/admin_auth.php';
|
||||||
|
require_once __DIR__ . '/../includes/meta_db.php';
|
||||||
|
require_once __DIR__ . '/../includes/i18n.php';
|
||||||
|
|
||||||
|
// ─── Actions POST ──────────────────────────────────────────────────────────────
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$action = $_POST['action'] ?? '';
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
|
||||||
|
if ($action === 'toggle_user' && $id) {
|
||||||
|
$meta_pdo->prepare("UPDATE users SET is_active = 1 - is_active WHERE id = ?")->execute([$id]);
|
||||||
|
} elseif ($action === 'toggle_family' && $id) {
|
||||||
|
$meta_pdo->prepare("UPDATE families SET is_active = 1 - is_active WHERE id = ?")->execute([$id]);
|
||||||
|
} elseif ($action === 'toggle_admin' && $id) {
|
||||||
|
// Ne peut pas se retirer ses propres droits admin
|
||||||
|
if ($id !== (int)$_SESSION['user']['id']) {
|
||||||
|
$meta_pdo->prepare("UPDATE users SET is_admin = 1 - is_admin WHERE id = ?")->execute([$id]);
|
||||||
|
}
|
||||||
|
} elseif ($action === 'delete_user' && $id) {
|
||||||
|
if ($id !== (int)$_SESSION['user']['id']) {
|
||||||
|
$meta_pdo->prepare("DELETE FROM users WHERE id = ?")->execute([$id]);
|
||||||
|
}
|
||||||
|
} elseif ($action === 'regen_invite' && $id) {
|
||||||
|
$new_code = bin2hex(random_bytes(8));
|
||||||
|
$meta_pdo->prepare("UPDATE families SET invite_code = ? WHERE id = ?")->execute([$new_code, $id]);
|
||||||
|
}
|
||||||
|
header('Location: /admin/');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Données ──────────────────────────────────────────────────────────────────
|
||||||
|
$families = $meta_pdo->query("
|
||||||
|
SELECT f.*, COUNT(u.id) as member_count
|
||||||
|
FROM families f
|
||||||
|
LEFT JOIN users u ON u.family_id = f.id
|
||||||
|
GROUP BY f.id
|
||||||
|
ORDER BY f.id
|
||||||
|
")->fetchAll();
|
||||||
|
|
||||||
|
$users = $meta_pdo->query("
|
||||||
|
SELECT u.*, f.name as family_name
|
||||||
|
FROM users u
|
||||||
|
LEFT JOIN families f ON f.id = u.family_id
|
||||||
|
ORDER BY u.id
|
||||||
|
")->fetchAll();
|
||||||
|
|
||||||
|
$pageTitle = "Admin — HouseHub";
|
||||||
|
$activePage = "admin";
|
||||||
|
require __DIR__ . '/../header.php';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="pf-container" style="max-width:1000px;margin:32px auto;padding:0 16px">
|
||||||
|
|
||||||
|
<h1 style="font-size:1.5rem;font-weight:700;margin-bottom:4px">⚙️ Panneau d'administration</h1>
|
||||||
|
<p style="color:#64748b;margin-bottom:32px">Gestion des espaces familiaux et des utilisateurs</p>
|
||||||
|
|
||||||
|
<!-- ── Familles ─────────────────────────────────────────────────────────── -->
|
||||||
|
<h2 style="font-size:1.1rem;font-weight:600;margin-bottom:16px">
|
||||||
|
🏠 Espaces familiaux
|
||||||
|
<span style="background:#e2e8f0;border-radius:12px;padding:2px 10px;font-size:0.85rem;font-weight:500;margin-left:8px"><?= count($families) ?></span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div style="overflow-x:auto;margin-bottom:40px">
|
||||||
|
<table style="width:100%;border-collapse:collapse;font-size:0.9rem">
|
||||||
|
<thead>
|
||||||
|
<tr style="background:#f8fafc;text-align:left">
|
||||||
|
<th style="padding:10px 14px;border-bottom:2px solid #e2e8f0">#</th>
|
||||||
|
<th style="padding:10px 14px;border-bottom:2px solid #e2e8f0">Nom</th>
|
||||||
|
<th style="padding:10px 14px;border-bottom:2px solid #e2e8f0">DB</th>
|
||||||
|
<th style="padding:10px 14px;border-bottom:2px solid #e2e8f0">Code invitation</th>
|
||||||
|
<th style="padding:10px 14px;border-bottom:2px solid #e2e8f0">Membres</th>
|
||||||
|
<th style="padding:10px 14px;border-bottom:2px solid #e2e8f0">Statut</th>
|
||||||
|
<th style="padding:10px 14px;border-bottom:2px solid #e2e8f0">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($families as $f): ?>
|
||||||
|
<tr style="border-bottom:1px solid #f1f5f9">
|
||||||
|
<td style="padding:10px 14px;color:#94a3b8"><?= $f['id'] ?></td>
|
||||||
|
<td style="padding:10px 14px;font-weight:600"><?= htmlspecialchars($f['name']) ?></td>
|
||||||
|
<td style="padding:10px 14px"><code style="font-size:0.8rem;background:#f1f5f9;padding:2px 6px;border-radius:4px"><?= $f['db_name'] ?></code></td>
|
||||||
|
<td style="padding:10px 14px">
|
||||||
|
<code style="font-size:0.85rem"><?= $f['invite_code'] ?></code>
|
||||||
|
</td>
|
||||||
|
<td style="padding:10px 14px;text-align:center"><?= $f['member_count'] ?></td>
|
||||||
|
<td style="padding:10px 14px">
|
||||||
|
<span style="padding:3px 10px;border-radius:12px;font-size:0.8rem;font-weight:600;
|
||||||
|
background:<?= $f['is_active'] ? '#dcfce7' : '#fee2e2' ?>;
|
||||||
|
color:<?= $f['is_active'] ? '#16a34a' : '#dc2626' ?>">
|
||||||
|
<?= $f['is_active'] ? 'Actif' : 'Inactif' ?>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td style="padding:10px 14px;display:flex;gap:8px;flex-wrap:wrap">
|
||||||
|
<form method="post" style="display:inline">
|
||||||
|
<input type="hidden" name="action" value="toggle_family">
|
||||||
|
<input type="hidden" name="id" value="<?= $f['id'] ?>">
|
||||||
|
<button class="pf-btn btn-secondary" style="padding:4px 12px;font-size:0.8rem">
|
||||||
|
<?= $f['is_active'] ? 'Désactiver' : 'Activer' ?>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" style="display:inline">
|
||||||
|
<input type="hidden" name="action" value="regen_invite">
|
||||||
|
<input type="hidden" name="id" value="<?= $f['id'] ?>">
|
||||||
|
<button class="pf-btn btn-secondary" style="padding:4px 12px;font-size:0.8rem"
|
||||||
|
onclick="return confirm('Regénérer le code ?')">
|
||||||
|
🔄 Code
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Utilisateurs ─────────────────────────────────────────────────────── -->
|
||||||
|
<h2 style="font-size:1.1rem;font-weight:600;margin-bottom:16px">
|
||||||
|
👤 Utilisateurs
|
||||||
|
<span style="background:#e2e8f0;border-radius:12px;padding:2px 10px;font-size:0.85rem;font-weight:500;margin-left:8px"><?= count($users) ?></span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div style="overflow-x:auto">
|
||||||
|
<table style="width:100%;border-collapse:collapse;font-size:0.9rem">
|
||||||
|
<thead>
|
||||||
|
<tr style="background:#f8fafc;text-align:left">
|
||||||
|
<th style="padding:10px 14px;border-bottom:2px solid #e2e8f0">#</th>
|
||||||
|
<th style="padding:10px 14px;border-bottom:2px solid #e2e8f0">Utilisateur</th>
|
||||||
|
<th style="padding:10px 14px;border-bottom:2px solid #e2e8f0">Espace famille</th>
|
||||||
|
<th style="padding:10px 14px;border-bottom:2px solid #e2e8f0">Rôle</th>
|
||||||
|
<th style="padding:10px 14px;border-bottom:2px solid #e2e8f0">Statut</th>
|
||||||
|
<th style="padding:10px 14px;border-bottom:2px solid #e2e8f0">Créé le</th>
|
||||||
|
<th style="padding:10px 14px;border-bottom:2px solid #e2e8f0">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($users as $u): $isSelf = $u['id'] == $_SESSION['user']['id']; ?>
|
||||||
|
<tr style="border-bottom:1px solid #f1f5f9<?= $isSelf ? ';background:#fafff4' : '' ?>">
|
||||||
|
<td style="padding:10px 14px;color:#94a3b8"><?= $u['id'] ?></td>
|
||||||
|
<td style="padding:10px 14px">
|
||||||
|
<strong><?= htmlspecialchars($u['display_name']) ?></strong>
|
||||||
|
<span style="color:#94a3b8;font-size:0.85rem;margin-left:6px">@<?= htmlspecialchars($u['username']) ?></span>
|
||||||
|
<?php if ($isSelf): ?><span style="font-size:0.75rem;color:#2563eb;margin-left:4px">(vous)</span><?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td style="padding:10px 14px"><?= $u['family_name'] ? htmlspecialchars($u['family_name']) : '<span style="color:#94a3b8">—</span>' ?></td>
|
||||||
|
<td style="padding:10px 14px">
|
||||||
|
<?php if ($u['is_admin']): ?>
|
||||||
|
<span style="background:#dbeafe;color:#1d4ed8;padding:2px 8px;border-radius:10px;font-size:0.8rem;font-weight:600">Admin</span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span style="color:#94a3b8;font-size:0.85rem">Membre</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td style="padding:10px 14px">
|
||||||
|
<span style="padding:3px 10px;border-radius:12px;font-size:0.8rem;font-weight:600;
|
||||||
|
background:<?= $u['is_active'] ? '#dcfce7' : '#fee2e2' ?>;
|
||||||
|
color:<?= $u['is_active'] ? '#16a34a' : '#dc2626' ?>">
|
||||||
|
<?= $u['is_active'] ? 'Actif' : 'Inactif' ?>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td style="padding:10px 14px;color:#64748b;font-size:0.85rem"><?= date('d/m/Y', strtotime($u['created_at'])) ?></td>
|
||||||
|
<td style="padding:10px 14px">
|
||||||
|
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
||||||
|
<?php if (!$isSelf): ?>
|
||||||
|
<form method="post" style="display:inline">
|
||||||
|
<input type="hidden" name="action" value="toggle_user">
|
||||||
|
<input type="hidden" name="id" value="<?= $u['id'] ?>">
|
||||||
|
<button class="pf-btn btn-secondary" style="padding:4px 10px;font-size:0.8rem">
|
||||||
|
<?= $u['is_active'] ? 'Désactiver' : 'Activer' ?>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" style="display:inline">
|
||||||
|
<input type="hidden" name="action" value="toggle_admin">
|
||||||
|
<input type="hidden" name="id" value="<?= $u['id'] ?>">
|
||||||
|
<button class="pf-btn btn-secondary" style="padding:4px 10px;font-size:0.8rem">
|
||||||
|
<?= $u['is_admin'] ? '↓ Membre' : '↑ Admin' ?>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" style="display:inline" onsubmit="return confirm('Supprimer <?= htmlspecialchars($u['username']) ?> ?')">
|
||||||
|
<input type="hidden" name="action" value="delete_user">
|
||||||
|
<input type="hidden" name="id" value="<?= $u['id'] ?>">
|
||||||
|
<button class="pf-btn btn-secondary" style="padding:4px 10px;font-size:0.8rem;color:#dc2626">🗑</button>
|
||||||
|
</form>
|
||||||
|
<?php else: ?>
|
||||||
|
<span style="color:#94a3b8;font-size:0.8rem">—</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php require __DIR__ . '/../footer.php'; ?>
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
-- Initialisation HouseHub — Meta DB + permissions
|
-- Initialisation HouseHub — Meta DB + permissions
|
||||||
-- Exécuté automatiquement au premier démarrage MariaDB
|
-- Exécuté automatiquement au premier démarrage MariaDB
|
||||||
|
|
||||||
-- Tables meta (dans la DB créée par MYSQL_DATABASE=househub_meta)
|
|
||||||
USE househub_meta;
|
USE househub_meta;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS families (
|
CREATE TABLE IF NOT EXISTS families (
|
||||||
@@ -9,6 +8,7 @@ CREATE TABLE IF NOT EXISTS families (
|
|||||||
name VARCHAR(100) NOT NULL,
|
name VARCHAR(100) NOT NULL,
|
||||||
db_name VARCHAR(64) NOT NULL UNIQUE,
|
db_name VARCHAR(64) NOT NULL UNIQUE,
|
||||||
invite_code VARCHAR(32) NOT NULL UNIQUE,
|
invite_code VARCHAR(32) NOT NULL UNIQUE,
|
||||||
|
is_active TINYINT(1) DEFAULT 1,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
@@ -18,6 +18,8 @@ CREATE TABLE IF NOT EXISTS users (
|
|||||||
password_hash VARCHAR(255) NOT NULL,
|
password_hash VARCHAR(255) NOT NULL,
|
||||||
display_name VARCHAR(100) NOT NULL,
|
display_name VARCHAR(100) NOT NULL,
|
||||||
family_id INT DEFAULT NULL,
|
family_id INT DEFAULT NULL,
|
||||||
|
is_admin TINYINT(1) DEFAULT 0,
|
||||||
|
is_active TINYINT(1) DEFAULT 1,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY (family_id) REFERENCES families(id)
|
FOREIGN KEY (family_id) REFERENCES families(id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/auth.php';
|
||||||
|
require_login();
|
||||||
|
|
||||||
|
if (empty($_SESSION['user']['is_admin'])) {
|
||||||
|
http_response_code(403);
|
||||||
|
die('<h1>403 — Accès réservé aux administrateurs</h1><a href="/">Retour</a>');
|
||||||
|
}
|
||||||
-64
@@ -1,64 +0,0 @@
|
|||||||
<?php
|
|
||||||
require __DIR__ . '/includes/auth.php';
|
|
||||||
require_login();
|
|
||||||
require_once __DIR__ . '/includes/meta_db.php';
|
|
||||||
|
|
||||||
$family = $meta_pdo->prepare("SELECT * FROM families WHERE id = ?");
|
|
||||||
$family->execute([$_SESSION['user']['family_id']]);
|
|
||||||
$family = $family->fetch();
|
|
||||||
|
|
||||||
$members = $meta_pdo->prepare("SELECT display_name, username, created_at FROM users WHERE family_id = ? ORDER BY id");
|
|
||||||
$members->execute([$_SESSION['user']['family_id']]);
|
|
||||||
$members = $members->fetchAll();
|
|
||||||
|
|
||||||
$pageTitle = "Invitation — HouseHub";
|
|
||||||
$activePage = "invite";
|
|
||||||
require __DIR__ . '/header.php';
|
|
||||||
?>
|
|
||||||
|
|
||||||
<div class="pf-container" style="max-width:600px;margin:40px auto;padding:0 16px">
|
|
||||||
|
|
||||||
<h1 style="font-size:1.4rem;font-weight:700;margin-bottom:8px">👨👩👧 Votre espace familial</h1>
|
|
||||||
<p style="color:#64748b;margin-bottom:32px">Partagez le code ci-dessous pour inviter quelqu'un dans votre espace.</p>
|
|
||||||
|
|
||||||
<div style="background:#f8fafc;border:2px dashed #cbd5e1;border-radius:12px;padding:24px;text-align:center;margin-bottom:32px">
|
|
||||||
<p style="font-size:0.85rem;color:#64748b;margin-bottom:8px">Famille : <strong><?= htmlspecialchars($family['name']) ?></strong></p>
|
|
||||||
<p style="font-size:0.8rem;color:#94a3b8;margin-bottom:16px">Code d'invitation</p>
|
|
||||||
<code id="invite-code" style="font-size:1.4rem;font-weight:700;letter-spacing:2px;color:#1e40af;cursor:pointer"
|
|
||||||
onclick="copyCode()" title="Cliquer pour copier">
|
|
||||||
<?= htmlspecialchars($family['invite_code']) ?>
|
|
||||||
</code>
|
|
||||||
<p id="copy-msg" style="color:#16a34a;font-size:0.85rem;margin-top:8px;opacity:0;transition:opacity .3s">✓ Copié !</p>
|
|
||||||
<p style="font-size:0.8rem;color:#94a3b8;margin-top:12px">
|
|
||||||
Lien d'inscription :
|
|
||||||
<a href="/register.php" style="color:var(--primary)"><?= $_SERVER['HTTP_HOST'] ?>/register.php</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 style="font-size:1.1rem;font-weight:600;margin-bottom:16px">Membres de l'espace (<?= count($members) ?>)</h2>
|
|
||||||
<div style="display:flex;flex-direction:column;gap:8px">
|
|
||||||
<?php foreach ($members as $m): ?>
|
|
||||||
<div style="background:white;border:1px solid #e2e8f0;border-radius:8px;padding:12px 16px;display:flex;justify-content:space-between;align-items:center">
|
|
||||||
<div>
|
|
||||||
<strong><?= htmlspecialchars($m['display_name']) ?></strong>
|
|
||||||
<span style="color:#94a3b8;font-size:0.85rem;margin-left:8px">@<?= htmlspecialchars($m['username']) ?></span>
|
|
||||||
</div>
|
|
||||||
<span style="font-size:0.8rem;color:#94a3b8"><?= date('d/m/Y', strtotime($m['created_at'])) ?></span>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function copyCode() {
|
|
||||||
const code = document.getElementById('invite-code').textContent.trim();
|
|
||||||
navigator.clipboard.writeText(code).then(() => {
|
|
||||||
const msg = document.getElementById('copy-msg');
|
|
||||||
msg.style.opacity = '1';
|
|
||||||
setTimeout(() => msg.style.opacity = '0', 2000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<?php require __DIR__ . '/footer.php'; ?>
|
|
||||||
@@ -21,7 +21,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$error = tr('error_missing_fields');
|
$error = tr('error_missing_fields');
|
||||||
} else {
|
} else {
|
||||||
$stmt = $meta_pdo->prepare("
|
$stmt = $meta_pdo->prepare("
|
||||||
SELECT u.id, u.username, u.password_hash, u.display_name, u.family_id, f.db_name
|
SELECT u.id, u.username, u.password_hash, u.display_name, u.family_id, u.is_admin, u.is_active, f.db_name, f.is_active as family_active
|
||||||
FROM users u
|
FROM users u
|
||||||
LEFT JOIN families f ON f.id = u.family_id
|
LEFT JOIN families f ON f.id = u.family_id
|
||||||
WHERE u.username = ?
|
WHERE u.username = ?
|
||||||
@@ -30,16 +30,23 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
if ($user && password_verify($password, $user['password_hash'])) {
|
if ($user && password_verify($password, $user['password_hash'])) {
|
||||||
$_SESSION['user'] = [
|
if (!$user['is_active']) {
|
||||||
'id' => (int)$user['id'],
|
$error = "Compte désactivé. Contactez l'administrateur.";
|
||||||
'username' => $user['username'],
|
} elseif ($user['family_id'] && !$user['family_active']) {
|
||||||
'display_name' => $user['display_name'],
|
$error = "Espace familial désactivé. Contactez l'administrateur.";
|
||||||
'family_id' => (int)$user['family_id'],
|
} else {
|
||||||
];
|
$_SESSION['user'] = [
|
||||||
$_SESSION['family_db'] = $user['db_name'];
|
'id' => (int)$user['id'],
|
||||||
$redirectTo = $_GET['redirect'] ?? '/index.php';
|
'username' => $user['username'],
|
||||||
header('Location: ' . $redirectTo);
|
'display_name' => $user['display_name'],
|
||||||
exit;
|
'family_id' => (int)$user['family_id'],
|
||||||
|
'is_admin' => (bool)$user['is_admin'],
|
||||||
|
];
|
||||||
|
$_SESSION['family_db'] = $user['db_name'];
|
||||||
|
$redirectTo = $_GET['redirect'] ?? '/index.php';
|
||||||
|
header('Location: ' . $redirectTo);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$error = tr('error_invalid_credentials');
|
$error = tr('error_invalid_credentials');
|
||||||
}
|
}
|
||||||
|
|||||||
+215
@@ -0,0 +1,215 @@
|
|||||||
|
<?php
|
||||||
|
require __DIR__ . '/includes/auth.php';
|
||||||
|
require_login();
|
||||||
|
require_once __DIR__ . '/includes/meta_db.php';
|
||||||
|
require_once __DIR__ . '/includes/i18n.php';
|
||||||
|
|
||||||
|
$user_id = $_SESSION['user']['id'];
|
||||||
|
$family_id = $_SESSION['user']['family_id'];
|
||||||
|
|
||||||
|
$error = null;
|
||||||
|
$success = null;
|
||||||
|
|
||||||
|
// ─── Actions ──────────────────────────────────────────────────────────────────
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$action = $_POST['action'] ?? '';
|
||||||
|
|
||||||
|
if ($action === 'update_profile') {
|
||||||
|
$display_name = trim($_POST['display_name'] ?? '');
|
||||||
|
if (!$display_name) {
|
||||||
|
$error = "Le prénom ne peut pas être vide.";
|
||||||
|
} else {
|
||||||
|
$meta_pdo->prepare("UPDATE users SET display_name = ? WHERE id = ?")
|
||||||
|
->execute([$display_name, $user_id]);
|
||||||
|
$_SESSION['user']['display_name'] = $display_name;
|
||||||
|
$success = "Profil mis à jour.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'change_password') {
|
||||||
|
$current = $_POST['current_password'] ?? '';
|
||||||
|
$new = $_POST['new_password'] ?? '';
|
||||||
|
$confirm = $_POST['confirm_password'] ?? '';
|
||||||
|
|
||||||
|
$row = $meta_pdo->prepare("SELECT password_hash FROM users WHERE id = ?");
|
||||||
|
$row->execute([$user_id]);
|
||||||
|
$row = $row->fetch();
|
||||||
|
|
||||||
|
if (!password_verify($current, $row['password_hash'])) {
|
||||||
|
$error = "Mot de passe actuel incorrect.";
|
||||||
|
} elseif (strlen($new) < 6) {
|
||||||
|
$error = "Le nouveau mot de passe doit faire au moins 6 caractères.";
|
||||||
|
} elseif ($new !== $confirm) {
|
||||||
|
$error = "Les mots de passe ne correspondent pas.";
|
||||||
|
} else {
|
||||||
|
$meta_pdo->prepare("UPDATE users SET password_hash = ? WHERE id = ?")
|
||||||
|
->execute([password_hash($new, PASSWORD_BCRYPT), $user_id]);
|
||||||
|
$success = "Mot de passe modifié avec succès.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'update_family_name' && $family_id) {
|
||||||
|
$name = trim($_POST['family_name'] ?? '');
|
||||||
|
if ($name) {
|
||||||
|
$meta_pdo->prepare("UPDATE families SET name = ? WHERE id = ?")
|
||||||
|
->execute([$name, $family_id]);
|
||||||
|
$success = "Nom de l'espace mis à jour.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'regen_invite' && $family_id) {
|
||||||
|
$new_code = bin2hex(random_bytes(8));
|
||||||
|
$meta_pdo->prepare("UPDATE families SET invite_code = ? WHERE id = ?")
|
||||||
|
->execute([$new_code, $family_id]);
|
||||||
|
$success = "Nouveau code d'invitation généré.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Chargement données ───────────────────────────────────────────────────────
|
||||||
|
$user = $meta_pdo->prepare("SELECT * FROM users WHERE id = ?");
|
||||||
|
$user->execute([$user_id]);
|
||||||
|
$user = $user->fetch();
|
||||||
|
|
||||||
|
$family = null;
|
||||||
|
$members = [];
|
||||||
|
if ($family_id) {
|
||||||
|
$fam = $meta_pdo->prepare("SELECT * FROM families WHERE id = ?");
|
||||||
|
$fam->execute([$family_id]);
|
||||||
|
$family = $fam->fetch();
|
||||||
|
|
||||||
|
$mem = $meta_pdo->prepare("SELECT display_name, username, created_at, is_admin FROM users WHERE family_id = ? ORDER BY id");
|
||||||
|
$mem->execute([$family_id]);
|
||||||
|
$members = $mem->fetchAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
$pageTitle = "Paramètres — HouseHub";
|
||||||
|
$activePage = "settings";
|
||||||
|
require __DIR__ . '/header.php';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="pf-container" style="max-width:680px;margin:32px auto;padding:0 16px">
|
||||||
|
|
||||||
|
<h1 style="font-size:1.4rem;font-weight:700;margin-bottom:32px">⚙️ Paramètres</h1>
|
||||||
|
|
||||||
|
<?php if ($success): ?>
|
||||||
|
<div style="background:#dcfce7;border:1px solid #86efac;border-radius:8px;padding:12px 16px;margin-bottom:24px;color:#166534">✓ <?= htmlspecialchars($success) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($error): ?>
|
||||||
|
<div style="background:#fee2e2;border:1px solid #fca5a5;border-radius:8px;padding:12px 16px;margin-bottom:24px;color:#991b1b">✗ <?= htmlspecialchars($error) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- ── Profil ──────────────────────────────────────────────────────────── -->
|
||||||
|
<section style="background:white;border:1px solid #e2e8f0;border-radius:12px;padding:24px;margin-bottom:24px">
|
||||||
|
<h2 style="font-size:1rem;font-weight:700;margin-bottom:20px">👤 Mon profil</h2>
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" name="action" value="update_profile">
|
||||||
|
<div class="pf-form-group">
|
||||||
|
<label class="pf-label">Nom d'utilisateur</label>
|
||||||
|
<input type="text" class="pf-input" value="<?= htmlspecialchars($user['username']) ?>" disabled style="background:#f8fafc;color:#94a3b8">
|
||||||
|
</div>
|
||||||
|
<div class="pf-form-group">
|
||||||
|
<label class="pf-label">Prénom / Pseudo affiché</label>
|
||||||
|
<input type="text" name="display_name" class="pf-input" value="<?= htmlspecialchars($user['display_name']) ?>" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="pf-btn">Enregistrer</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ── Mot de passe ────────────────────────────────────────────────────── -->
|
||||||
|
<section style="background:white;border:1px solid #e2e8f0;border-radius:12px;padding:24px;margin-bottom:24px">
|
||||||
|
<h2 style="font-size:1rem;font-weight:700;margin-bottom:20px">🔒 Changer le mot de passe</h2>
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" name="action" value="change_password">
|
||||||
|
<div class="pf-form-group">
|
||||||
|
<label class="pf-label">Mot de passe actuel</label>
|
||||||
|
<input type="password" name="current_password" class="pf-input" required placeholder="••••••">
|
||||||
|
</div>
|
||||||
|
<div class="pf-form-group">
|
||||||
|
<label class="pf-label">Nouveau mot de passe</label>
|
||||||
|
<input type="password" name="new_password" class="pf-input" required placeholder="••••••" autocomplete="new-password">
|
||||||
|
</div>
|
||||||
|
<div class="pf-form-group">
|
||||||
|
<label class="pf-label">Confirmer</label>
|
||||||
|
<input type="password" name="confirm_password" class="pf-input" required placeholder="••••••" autocomplete="new-password">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="pf-btn">Modifier</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<?php if ($family): ?>
|
||||||
|
<!-- ── Espace familial ─────────────────────────────────────────────────── -->
|
||||||
|
<section style="background:white;border:1px solid #e2e8f0;border-radius:12px;padding:24px;margin-bottom:24px">
|
||||||
|
<h2 style="font-size:1rem;font-weight:700;margin-bottom:20px">🏠 Mon espace familial</h2>
|
||||||
|
|
||||||
|
<form method="post" style="margin-bottom:20px">
|
||||||
|
<input type="hidden" name="action" value="update_family_name">
|
||||||
|
<div class="pf-form-group">
|
||||||
|
<label class="pf-label">Nom de l'espace</label>
|
||||||
|
<div style="display:flex;gap:8px">
|
||||||
|
<input type="text" name="family_name" class="pf-input" value="<?= htmlspecialchars($family['name']) ?>" required>
|
||||||
|
<button type="submit" class="pf-btn" style="flex-shrink:0">Enregistrer</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Code d'invitation -->
|
||||||
|
<div style="background:#f8fafc;border:2px dashed #cbd5e1;border-radius:10px;padding:20px">
|
||||||
|
<p style="font-size:0.85rem;color:#64748b;margin-bottom:8px">Code d'invitation — partagez-le pour inviter quelqu'un</p>
|
||||||
|
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap">
|
||||||
|
<code id="invite-code" style="font-size:1.3rem;font-weight:700;letter-spacing:3px;color:#1e40af;cursor:pointer"
|
||||||
|
onclick="copyCode()" title="Cliquer pour copier">
|
||||||
|
<?= htmlspecialchars($family['invite_code']) ?>
|
||||||
|
</code>
|
||||||
|
<span id="copy-msg" style="color:#16a34a;font-size:0.85rem;opacity:0;transition:opacity .3s">✓ Copié !</span>
|
||||||
|
<form method="post" style="margin-left:auto">
|
||||||
|
<input type="hidden" name="action" value="regen_invite">
|
||||||
|
<button type="submit" class="pf-btn btn-secondary" style="font-size:0.85rem"
|
||||||
|
onclick="return confirm('Regénérer le code ? L\'ancien sera invalidé.')">
|
||||||
|
🔄 Nouveau code
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<p style="margin-top:12px;font-size:0.8rem;color:#94a3b8">
|
||||||
|
Lien d'inscription : <strong><?= $_SERVER['HTTP_HOST'] ?>/register.php</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Membres -->
|
||||||
|
<h3 style="font-size:0.95rem;font-weight:600;margin:20px 0 12px">Membres (<?= count($members) ?>)</h3>
|
||||||
|
<div style="display:flex;flex-direction:column;gap:8px">
|
||||||
|
<?php foreach ($members as $m): ?>
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;padding:10px 14px;background:#f8fafc;border-radius:8px">
|
||||||
|
<div>
|
||||||
|
<strong><?= htmlspecialchars($m['display_name']) ?></strong>
|
||||||
|
<span style="color:#94a3b8;font-size:0.85rem;margin-left:6px">@<?= htmlspecialchars($m['username']) ?></span>
|
||||||
|
<?php if ($m['is_admin']): ?>
|
||||||
|
<span style="background:#dbeafe;color:#1d4ed8;padding:1px 7px;border-radius:8px;font-size:0.75rem;font-weight:600;margin-left:6px">Admin</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<span style="font-size:0.8rem;color:#94a3b8"><?= date('d/m/Y', strtotime($m['created_at'])) ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($_SESSION['user']['is_admin']): ?>
|
||||||
|
<div style="text-align:center;margin-top:8px">
|
||||||
|
<a href="/admin/" style="color:#2563eb;font-size:0.9rem">→ Panneau d'administration</a>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function copyCode() {
|
||||||
|
const code = document.getElementById('invite-code').textContent.trim();
|
||||||
|
navigator.clipboard.writeText(code).then(() => {
|
||||||
|
const msg = document.getElementById('copy-msg');
|
||||||
|
msg.style.opacity = '1';
|
||||||
|
setTimeout(() => msg.style.opacity = '0', 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<?php require __DIR__ . '/footer.php'; ?>
|
||||||
Reference in New Issue
Block a user