feat: système multi-tenant (familles + inscription + invitations)
Deploy to Pacha Family / deploy (push) Failing after 3s
Deploy to Pacha Family / deploy (push) Failing after 3s
- docker/init/00-setup.sql : meta DB househub_meta (families + users) + grants - docker/schema_family.sql : template DB par famille (toutes les pf_* tables) - docker-compose.yml : MYSQL_DATABASE=househub_meta, init dir monté - includes/meta_db.php : PDO vers househub_meta - includes/db.php : connexion dynamique vers la DB famille (session) - login.php : auth via meta DB + family_db en session + lien inscription - register.php : inscription + création famille OU rejoindre via code - invite.php : voir son code d'invitation + membres de l'espace Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
634e4647da
commit
84db4b6619
+2
-2
@@ -29,12 +29,12 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASS:-rootchangeme}
|
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASS:-rootchangeme}
|
||||||
MYSQL_DATABASE: ${DB_NAME:-househub}
|
MYSQL_DATABASE: househub_meta
|
||||||
MYSQL_USER: ${DB_USER:-househub}
|
MYSQL_USER: ${DB_USER:-househub}
|
||||||
MYSQL_PASSWORD: ${DB_PASS:-changeme}
|
MYSQL_PASSWORD: ${DB_PASS:-changeme}
|
||||||
volumes:
|
volumes:
|
||||||
- househub_db_data:/var/lib/mysql
|
- househub_db_data:/var/lib/mysql
|
||||||
- ./schema.sql:/docker-entrypoint-initdb.d/schema.sql:ro
|
- ./docker/init:/docker-entrypoint-initdb.d:ro
|
||||||
networks:
|
networks:
|
||||||
- househub_net
|
- househub_net
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
-- Initialisation HouseHub — Meta DB + permissions
|
||||||
|
-- Exécuté automatiquement au premier démarrage MariaDB
|
||||||
|
|
||||||
|
-- Tables meta (dans la DB créée par MYSQL_DATABASE=househub_meta)
|
||||||
|
USE househub_meta;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS families (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
db_name VARCHAR(64) NOT NULL UNIQUE,
|
||||||
|
invite_code VARCHAR(32) NOT NULL UNIQUE,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
username VARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
password_hash VARCHAR(255) NOT NULL,
|
||||||
|
display_name VARCHAR(100) NOT NULL,
|
||||||
|
family_id INT DEFAULT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (family_id) REFERENCES families(id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- Donner au user applicatif le droit de créer des DBs famille
|
||||||
|
GRANT ALL PRIVILEGES ON `househub_f%`.* TO 'househub'@'%';
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
-- HouseHub — Schéma MySQL
|
||||||
|
-- Créer la base : CREATE DATABASE househub CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
|
-- ─── Utilisateurs ─────────────────────────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_users (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
username VARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
password_hash VARCHAR(255) NOT NULL,
|
||||||
|
display_name VARCHAR(100) NOT NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- ─── Personnes ────────────────────────────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_people (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- ─── Calendrier familial ──────────────────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_events (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
event_date DATE NOT NULL,
|
||||||
|
event_type VARCHAR(50) NOT NULL,
|
||||||
|
person_id INT NOT NULL,
|
||||||
|
duration DECIMAL(4,2) DEFAULT 1.0
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_leaves (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
person_id INT NOT NULL,
|
||||||
|
leave_type VARCHAR(50) NOT NULL,
|
||||||
|
leave_date DATE NOT NULL,
|
||||||
|
duration DECIMAL(4,2) DEFAULT 1.0,
|
||||||
|
UNIQUE KEY uq_leave (person_id, leave_type, leave_date)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_leave_balances (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
person_id INT NOT NULL,
|
||||||
|
leave_type VARCHAR(50) NOT NULL,
|
||||||
|
initial_balance DECIMAL(6,2) DEFAULT 0,
|
||||||
|
balance_year INT NOT NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_leave_snapshots (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
person_id INT NOT NULL,
|
||||||
|
leave_type VARCHAR(50) NOT NULL,
|
||||||
|
snapshot_date DATE NOT NULL,
|
||||||
|
remaining_balance DECIMAL(6,2) DEFAULT 0
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_person_leave_meta (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
person_id INT NOT NULL UNIQUE,
|
||||||
|
anniversary_date DATE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_calendar_weeks (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
year INT NOT NULL,
|
||||||
|
week_iso_year INT NOT NULL,
|
||||||
|
week_iso_number INT NOT NULL,
|
||||||
|
week_label VARCHAR(20),
|
||||||
|
month INT,
|
||||||
|
month_name VARCHAR(20),
|
||||||
|
week_start_date DATE,
|
||||||
|
mon_date DATE, tue_date DATE, wed_date DATE,
|
||||||
|
thu_date DATE, fri_date DATE, sat_date DATE, sun_date DATE,
|
||||||
|
UNIQUE KEY uq_week (year, week_iso_year, week_iso_number)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- ─── Budget ───────────────────────────────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_budget_items (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
amount DECIMAL(10,2) DEFAULT 0,
|
||||||
|
category VARCHAR(100),
|
||||||
|
type VARCHAR(50),
|
||||||
|
payment_day INT DEFAULT NULL,
|
||||||
|
is_estimate TINYINT(1) DEFAULT 0,
|
||||||
|
reg_month VARCHAR(7) DEFAULT NULL,
|
||||||
|
mapping_keywords TEXT DEFAULT NULL,
|
||||||
|
holiday_id INT DEFAULT NULL,
|
||||||
|
is_checked TINYINT(1) DEFAULT 0,
|
||||||
|
sort_order INT DEFAULT 0
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_expenses (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
date_exp DATE NOT NULL,
|
||||||
|
gestion_month VARCHAR(7) NOT NULL,
|
||||||
|
category VARCHAR(100),
|
||||||
|
label VARCHAR(255),
|
||||||
|
amount DECIMAL(10,2) DEFAULT 0,
|
||||||
|
import_ref VARCHAR(255) DEFAULT NULL,
|
||||||
|
budget_item_id INT DEFAULT NULL,
|
||||||
|
holiday_id INT DEFAULT NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_alloc_categories (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
target DECIMAL(10,2) DEFAULT 0,
|
||||||
|
holiday_id INT DEFAULT NULL,
|
||||||
|
sort_order INT DEFAULT 0
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_alloc_values (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
month_date VARCHAR(7) NOT NULL,
|
||||||
|
cat_id INT NOT NULL,
|
||||||
|
amount_alex DECIMAL(10,2) DEFAULT 0,
|
||||||
|
amount_laia DECIMAL(10,2) DEFAULT 0,
|
||||||
|
UNIQUE KEY uq_alloc (month_date, cat_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_savings (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
owner VARCHAR(50) NOT NULL,
|
||||||
|
month_date VARCHAR(7) NOT NULL,
|
||||||
|
category VARCHAR(100) NOT NULL,
|
||||||
|
amount DECIMAL(10,2) DEFAULT 0,
|
||||||
|
holiday_id INT DEFAULT NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_bank_snapshots (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
snapshot_date DATE NOT NULL,
|
||||||
|
amount DECIMAL(12,2) DEFAULT 0
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_salary_config (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
year INT NOT NULL,
|
||||||
|
person VARCHAR(50) NOT NULL,
|
||||||
|
salary DECIMAL(10,2) DEFAULT 0,
|
||||||
|
mensualite DECIMAL(10,2) DEFAULT 0,
|
||||||
|
frais_func DECIMAL(10,2) DEFAULT 0,
|
||||||
|
eco_perso DECIMAL(10,2) DEFAULT 0,
|
||||||
|
eco_family DECIMAL(10,2) DEFAULT 0,
|
||||||
|
UNIQUE KEY uq_salary (year, person)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_import_rules (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
keyword VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
category VARCHAR(100) NOT NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_notes (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
note_type VARCHAR(100) NOT NULL,
|
||||||
|
reference_id VARCHAR(100) NOT NULL,
|
||||||
|
content TEXT,
|
||||||
|
UNIQUE KEY uq_note (note_type, reference_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- ─── Voyages / Holidays ───────────────────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_holidays (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
period_hint VARCHAR(100) DEFAULT NULL,
|
||||||
|
start_date DATE DEFAULT NULL,
|
||||||
|
end_date DATE DEFAULT NULL,
|
||||||
|
status VARCHAR(50) DEFAULT 'draft',
|
||||||
|
budget_food DECIMAL(10,2) DEFAULT 0,
|
||||||
|
budget_extra DECIMAL(10,2) DEFAULT 0,
|
||||||
|
notes TEXT DEFAULT NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_holidays_items (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
holiday_id INT NOT NULL,
|
||||||
|
category VARCHAR(100),
|
||||||
|
name VARCHAR(255),
|
||||||
|
amount DECIMAL(10,2) DEFAULT 0,
|
||||||
|
is_paid TINYINT(1) DEFAULT 0,
|
||||||
|
location_name VARCHAR(255) DEFAULT NULL,
|
||||||
|
lat DECIMAL(10,7) DEFAULT NULL,
|
||||||
|
lng DECIMAL(10,7) DEFAULT NULL,
|
||||||
|
sort_order INT DEFAULT 0,
|
||||||
|
notes TEXT DEFAULT NULL,
|
||||||
|
item_date DATE DEFAULT NULL,
|
||||||
|
item_time TIME DEFAULT NULL,
|
||||||
|
step_start_date DATE DEFAULT NULL,
|
||||||
|
step_end_date DATE DEFAULT NULL,
|
||||||
|
duration INT DEFAULT NULL,
|
||||||
|
is_return TINYINT(1) DEFAULT 0
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_holidays_ideas (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
period_hint VARCHAR(100) DEFAULT NULL,
|
||||||
|
start_date DATE DEFAULT NULL,
|
||||||
|
end_date DATE DEFAULT NULL,
|
||||||
|
status VARCHAR(50) DEFAULT 'idea',
|
||||||
|
budget_food DECIMAL(10,2) DEFAULT 0,
|
||||||
|
budget_extra DECIMAL(10,2) DEFAULT 0,
|
||||||
|
notes TEXT DEFAULT NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_geocode_cache (
|
||||||
|
q_hash CHAR(64) PRIMARY KEY,
|
||||||
|
q VARCHAR(255),
|
||||||
|
lat DECIMAL(10,7),
|
||||||
|
lng DECIMAL(10,7),
|
||||||
|
display_name TEXT,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- ─── Cadeaux ──────────────────────────────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_gifts (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
year INT NOT NULL,
|
||||||
|
adult_name VARCHAR(100) NOT NULL,
|
||||||
|
payer_name VARCHAR(100),
|
||||||
|
child_name VARCHAR(100) NOT NULL,
|
||||||
|
occasion VARCHAR(50) NOT NULL,
|
||||||
|
gift_description TEXT NOT NULL,
|
||||||
|
product_link VARCHAR(500) DEFAULT NULL,
|
||||||
|
amount DECIMAL(8,2) DEFAULT 0
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
|
|
||||||
|
-- ─── Données initiales ────────────────────────────────────────────────────────
|
||||||
|
-- Utilisateur admin (mot de passe : changeme → à modifier !)
|
||||||
|
INSERT IGNORE INTO pf_users (id, username, password_hash, display_name)
|
||||||
|
VALUES (1, 'admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Admin');
|
||||||
|
|
||||||
|
-- Personnes (IDs fixes correspondant aux constantes dans config.php)
|
||||||
|
INSERT IGNORE INTO pf_people (id, name) VALUES (2, 'Alex');
|
||||||
|
INSERT IGNORE INTO pf_people (id, name) VALUES (3, 'Laia');
|
||||||
+27
-15
@@ -3,26 +3,38 @@ if (file_exists(__DIR__ . '/config.php')) {
|
|||||||
require_once __DIR__ . '/config.php';
|
require_once __DIR__ . '/config.php';
|
||||||
}
|
}
|
||||||
|
|
||||||
$host = getenv('DB_HOST') ?: 'househub-db';
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
$db = getenv('DB_NAME') ?: 'househub';
|
session_start();
|
||||||
$user = getenv('DB_USER') ?: 'househub';
|
}
|
||||||
$pass = getenv('DB_PASS') ?: 'changeme';
|
|
||||||
$charset = 'utf8mb4';
|
|
||||||
|
|
||||||
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
|
$host = getenv('DB_HOST') ?: 'househub-db';
|
||||||
|
$user = getenv('DB_USER') ?: 'househub';
|
||||||
|
$pass = getenv('DB_PASS') ?: 'changeme';
|
||||||
|
$db = $_SESSION['family_db'] ?? null;
|
||||||
|
|
||||||
$options = [
|
if (!$db) {
|
||||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
$current = basename($_SERVER['PHP_SELF'] ?? '');
|
||||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
if (!in_array($current, ['login.php', 'register.php'])) {
|
||||||
PDO::ATTR_EMULATE_PREPARES => false,
|
header('Location: /login.php');
|
||||||
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_general_ci",
|
exit;
|
||||||
PDO::ATTR_TIMEOUT => 30,
|
}
|
||||||
];
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$pdo = new PDO($dsn, $user, $pass, $options);
|
$pdo = new PDO(
|
||||||
|
"mysql:host=$host;dbname=$db;charset=utf8mb4",
|
||||||
|
$user, $pass,
|
||||||
|
[
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
PDO::ATTR_EMULATE_PREPARES => false,
|
||||||
|
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_general_ci",
|
||||||
|
PDO::ATTR_TIMEOUT => 30,
|
||||||
|
]
|
||||||
|
);
|
||||||
$pdo->exec("SET collation_connection = utf8mb4_general_ci");
|
$pdo->exec("SET collation_connection = utf8mb4_general_ci");
|
||||||
} catch (\PDOException $e) {
|
} catch (\PDOException $e) {
|
||||||
die("Erreur de connexion BDD : " . $e->getMessage());
|
die("Erreur connexion BDD famille : " . $e->getMessage());
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
// Connexion à la base meta (gestion des familles et utilisateurs)
|
||||||
|
$meta_host = getenv('DB_HOST') ?: 'househub-db';
|
||||||
|
$meta_db = 'househub_meta';
|
||||||
|
$meta_user = getenv('DB_USER') ?: 'househub';
|
||||||
|
$meta_pass = getenv('DB_PASS') ?: 'changeme';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$meta_pdo = new PDO(
|
||||||
|
"mysql:host=$meta_host;dbname=$meta_db;charset=utf8mb4",
|
||||||
|
$meta_user,
|
||||||
|
$meta_pass,
|
||||||
|
[
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
PDO::ATTR_EMULATE_PREPARES => false,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} catch (\PDOException $e) {
|
||||||
|
die(json_encode(['error' => 'Meta DB unavailable: ' . $e->getMessage()]));
|
||||||
|
}
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
<?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'; ?>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
session_start();
|
session_start();
|
||||||
require __DIR__ . '/includes/db.php';
|
require_once __DIR__ . '/includes/i18n.php';
|
||||||
require_once __DIR__ . '/includes/i18n.php'; // Toujours s'assurer que tr() est dispo
|
require_once __DIR__ . '/includes/meta_db.php';
|
||||||
|
|
||||||
$pageTitle = tr('login_title');
|
$pageTitle = tr('login_title');
|
||||||
$activePage = "login";
|
$activePage = "login";
|
||||||
@@ -20,16 +20,23 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
if (!$username || !$password) {
|
if (!$username || !$password) {
|
||||||
$error = tr('error_missing_fields');
|
$error = tr('error_missing_fields');
|
||||||
} else {
|
} else {
|
||||||
$stmt = $pdo->prepare("SELECT id, username, password_hash, display_name FROM pf_users WHERE username = ?");
|
$stmt = $meta_pdo->prepare("
|
||||||
|
SELECT u.id, u.username, u.password_hash, u.display_name, u.family_id, f.db_name
|
||||||
|
FROM users u
|
||||||
|
LEFT JOIN families f ON f.id = u.family_id
|
||||||
|
WHERE u.username = ?
|
||||||
|
");
|
||||||
$stmt->execute([$username]);
|
$stmt->execute([$username]);
|
||||||
$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'] = [
|
$_SESSION['user'] = [
|
||||||
'id' => (int)$user['id'],
|
'id' => (int)$user['id'],
|
||||||
'username' => $user['username'],
|
'username' => $user['username'],
|
||||||
'display_name' => $user['display_name'],
|
'display_name' => $user['display_name'],
|
||||||
|
'family_id' => (int)$user['family_id'],
|
||||||
];
|
];
|
||||||
|
$_SESSION['family_db'] = $user['db_name'];
|
||||||
$redirectTo = $_GET['redirect'] ?? '/index.php';
|
$redirectTo = $_GET['redirect'] ?? '/index.php';
|
||||||
header('Location: ' . $redirectTo);
|
header('Location: ' . $redirectTo);
|
||||||
exit;
|
exit;
|
||||||
@@ -75,7 +82,11 @@ require __DIR__ . '/header.php';
|
|||||||
<?= tr('btn_login_submit') ?>
|
<?= tr('btn_login_submit') ?>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<p style="text-align:center;margin-top:20px;font-size:0.9rem;color:#64748b">
|
||||||
|
Pas encore de compte ? <a href="/register.php" style="color:var(--primary)">Créer un espace</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+251
@@ -0,0 +1,251 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
require_once __DIR__ . '/includes/i18n.php';
|
||||||
|
require_once __DIR__ . '/includes/meta_db.php';
|
||||||
|
|
||||||
|
if (isset($_SESSION['user'])) {
|
||||||
|
header('Location: /index.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$error = null;
|
||||||
|
$success = null;
|
||||||
|
|
||||||
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function createFamilyDb(PDO $meta, string $db_host, string $db_user, string $db_pass, int $family_id): string
|
||||||
|
{
|
||||||
|
$db_name = 'househub_f' . $family_id;
|
||||||
|
$schema = file_get_contents(__DIR__ . '/docker/schema_family.sql');
|
||||||
|
|
||||||
|
$root_pdo = new PDO(
|
||||||
|
"mysql:host=$db_host;charset=utf8mb4",
|
||||||
|
$db_user, $db_pass,
|
||||||
|
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
||||||
|
);
|
||||||
|
|
||||||
|
$root_pdo->exec("CREATE DATABASE IF NOT EXISTS `$db_name` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
|
||||||
|
$root_pdo->exec("USE `$db_name`");
|
||||||
|
|
||||||
|
// Exécuter le schéma instruction par instruction
|
||||||
|
foreach (array_filter(array_map('trim', explode(';', $schema))) as $stmt) {
|
||||||
|
if ($stmt !== '') {
|
||||||
|
try { $root_pdo->exec($stmt); } catch (\PDOException $e) { /* ignore DROP IF EXISTS warnings */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $db_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Traitement du formulaire ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$action = $_POST['action'] ?? 'create';
|
||||||
|
$username = trim($_POST['username'] ?? '');
|
||||||
|
$display_name = trim($_POST['display_name'] ?? '');
|
||||||
|
$password = $_POST['password'] ?? '';
|
||||||
|
$password2 = $_POST['password2'] ?? '';
|
||||||
|
$family_name = trim($_POST['family_name'] ?? '');
|
||||||
|
$invite_code = trim($_POST['invite_code'] ?? '');
|
||||||
|
|
||||||
|
// Validations communes
|
||||||
|
if (!$username || !$display_name || !$password) {
|
||||||
|
$error = "Tous les champs sont obligatoires.";
|
||||||
|
} elseif (strlen($password) < 6) {
|
||||||
|
$error = "Le mot de passe doit faire au moins 6 caractères.";
|
||||||
|
} elseif ($password !== $password2) {
|
||||||
|
$error = "Les mots de passe ne correspondent pas.";
|
||||||
|
} elseif (!preg_match('/^[a-zA-Z0-9_.-]{3,50}$/', $username)) {
|
||||||
|
$error = "Nom d'utilisateur invalide (3-50 car., lettres/chiffres/._-)";
|
||||||
|
} else {
|
||||||
|
// Vérifier que l'username n'existe pas
|
||||||
|
$chk = $meta_pdo->prepare("SELECT id FROM users WHERE username = ?");
|
||||||
|
$chk->execute([$username]);
|
||||||
|
if ($chk->fetch()) {
|
||||||
|
$error = "Ce nom d'utilisateur est déjà pris.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$error) {
|
||||||
|
$db_host = getenv('DB_HOST') ?: 'househub-db';
|
||||||
|
$db_user = getenv('DB_USER') ?: 'househub';
|
||||||
|
$db_pass = getenv('DB_PASS') ?: 'changeme';
|
||||||
|
$hash = password_hash($password, PASSWORD_BCRYPT);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($action === 'join' && $invite_code) {
|
||||||
|
// ── Rejoindre une famille existante ──────────────────────────
|
||||||
|
$fam = $meta_pdo->prepare("SELECT * FROM families WHERE invite_code = ?");
|
||||||
|
$fam->execute([$invite_code]);
|
||||||
|
$family = $fam->fetch();
|
||||||
|
|
||||||
|
if (!$family) {
|
||||||
|
$error = "Code d'invitation invalide.";
|
||||||
|
} else {
|
||||||
|
$meta_pdo->prepare(
|
||||||
|
"INSERT INTO users (username, password_hash, display_name, family_id) VALUES (?, ?, ?, ?)"
|
||||||
|
)->execute([$username, $hash, $display_name, $family['id']]);
|
||||||
|
|
||||||
|
$_SESSION['user'] = [
|
||||||
|
'id' => (int)$meta_pdo->lastInsertId(),
|
||||||
|
'username' => $username,
|
||||||
|
'display_name' => $display_name,
|
||||||
|
'family_id' => (int)$family['id'],
|
||||||
|
];
|
||||||
|
$_SESSION['family_db'] = $family['db_name'];
|
||||||
|
header('Location: /index.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// ── Créer une nouvelle famille ────────────────────────────────
|
||||||
|
if (!$family_name) {
|
||||||
|
$error = "Le nom de la famille est obligatoire.";
|
||||||
|
} else {
|
||||||
|
$meta_pdo->beginTransaction();
|
||||||
|
|
||||||
|
// Créer la famille avec un invite_code unique
|
||||||
|
$invite = bin2hex(random_bytes(8));
|
||||||
|
$meta_pdo->prepare(
|
||||||
|
"INSERT INTO families (name, db_name, invite_code) VALUES (?, '', ?)"
|
||||||
|
)->execute([$family_name, $invite]);
|
||||||
|
$family_id = (int)$meta_pdo->lastInsertId();
|
||||||
|
|
||||||
|
// Créer la DB famille
|
||||||
|
$db_name = createFamilyDb($meta_pdo, $db_host, $db_user, $db_pass, $family_id);
|
||||||
|
|
||||||
|
// Mettre à jour db_name maintenant qu'on a l'ID
|
||||||
|
$meta_pdo->prepare("UPDATE families SET db_name = ? WHERE id = ?")
|
||||||
|
->execute([$db_name, $family_id]);
|
||||||
|
|
||||||
|
// Créer l'utilisateur
|
||||||
|
$meta_pdo->prepare(
|
||||||
|
"INSERT INTO users (username, password_hash, display_name, family_id) VALUES (?, ?, ?, ?)"
|
||||||
|
)->execute([$username, $hash, $display_name, $family_id]);
|
||||||
|
$user_id = (int)$meta_pdo->lastInsertId();
|
||||||
|
|
||||||
|
$meta_pdo->commit();
|
||||||
|
|
||||||
|
$_SESSION['user'] = [
|
||||||
|
'id' => $user_id,
|
||||||
|
'username' => $username,
|
||||||
|
'display_name' => $display_name,
|
||||||
|
'family_id' => $family_id,
|
||||||
|
];
|
||||||
|
$_SESSION['family_db'] = $db_name;
|
||||||
|
header('Location: /index.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
if ($meta_pdo->inTransaction()) $meta_pdo->rollBack();
|
||||||
|
$error = "Erreur lors de la création : " . $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pageTitle = "Inscription — HouseHub";
|
||||||
|
$activePage = "register";
|
||||||
|
require __DIR__ . '/header.php';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="pf-container pf-login-wrapper">
|
||||||
|
<div class="pf-login-card" style="max-width:480px">
|
||||||
|
|
||||||
|
<header class="pf-login-header">
|
||||||
|
<img src="/favicon.png" alt="HouseHub Logo" class="pf-login-icon">
|
||||||
|
<h1>Créer un compte</h1>
|
||||||
|
<p>Nouvel espace familial ou rejoindre un existant</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<?php if ($error): ?>
|
||||||
|
<div class="pf-login-error"><?= htmlspecialchars($error) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- Onglets -->
|
||||||
|
<div style="display:flex;gap:8px;margin-bottom:24px">
|
||||||
|
<button id="tab-create" onclick="switchTab('create')"
|
||||||
|
class="pf-btn" style="flex:1">Nouvel espace</button>
|
||||||
|
<button id="tab-join" onclick="switchTab('join')"
|
||||||
|
class="pf-btn btn-secondary" style="flex:1">Rejoindre</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formulaire commun -->
|
||||||
|
<form method="post" action="/register.php" id="reg-form">
|
||||||
|
<input type="hidden" name="action" id="reg-action" value="create">
|
||||||
|
|
||||||
|
<div class="pf-form-group">
|
||||||
|
<label class="pf-label">Nom d'utilisateur</label>
|
||||||
|
<input type="text" name="username" class="pf-input" required
|
||||||
|
value="<?= htmlspecialchars($_POST['username'] ?? '') ?>"
|
||||||
|
placeholder="ex: perco" autocapitalize="none">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pf-form-group">
|
||||||
|
<label class="pf-label">Prénom / Pseudo affiché</label>
|
||||||
|
<input type="text" name="display_name" class="pf-input" required
|
||||||
|
value="<?= htmlspecialchars($_POST['display_name'] ?? '') ?>"
|
||||||
|
placeholder="ex: Perco">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pf-form-group">
|
||||||
|
<label class="pf-label">Mot de passe</label>
|
||||||
|
<input type="password" name="password" class="pf-input" required
|
||||||
|
placeholder="••••••" autocomplete="new-password">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pf-form-group">
|
||||||
|
<label class="pf-label">Confirmer le mot de passe</label>
|
||||||
|
<input type="password" name="password2" class="pf-input" required
|
||||||
|
placeholder="••••••" autocomplete="new-password">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Champs spécifiques : Nouvel espace -->
|
||||||
|
<div id="section-create">
|
||||||
|
<div class="pf-form-group">
|
||||||
|
<label class="pf-label">Nom de la famille / foyer</label>
|
||||||
|
<input type="text" name="family_name" class="pf-input"
|
||||||
|
value="<?= htmlspecialchars($_POST['family_name'] ?? '') ?>"
|
||||||
|
placeholder="ex: Famille Dupont">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Champs spécifiques : Rejoindre -->
|
||||||
|
<div id="section-join" style="display:none">
|
||||||
|
<div class="pf-form-group">
|
||||||
|
<label class="pf-label">Code d'invitation</label>
|
||||||
|
<input type="text" name="invite_code" class="pf-input"
|
||||||
|
value="<?= htmlspecialchars($_POST['invite_code'] ?? '') ?>"
|
||||||
|
placeholder="ex: a1b2c3d4e5f6g7h8" autocapitalize="none">
|
||||||
|
<small style="color:#64748b;font-size:0.8rem;margin-top:4px;display:block">
|
||||||
|
Demande ce code à la personne qui administre l'espace.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="pf-btn pf-btn-block" style="margin-top:8px">
|
||||||
|
Créer mon compte
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p style="text-align:center;margin-top:20px;font-size:0.9rem;color:#64748b">
|
||||||
|
Déjà un compte ? <a href="/login.php" style="color:var(--primary)">Se connecter</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function switchTab(tab) {
|
||||||
|
document.getElementById('section-create').style.display = tab === 'create' ? '' : 'none';
|
||||||
|
document.getElementById('section-join').style.display = tab === 'join' ? '' : 'none';
|
||||||
|
document.getElementById('reg-action').value = tab;
|
||||||
|
document.getElementById('tab-create').className = 'pf-btn' + (tab === 'create' ? '' : ' btn-secondary');
|
||||||
|
document.getElementById('tab-join').className = 'pf-btn' + (tab === 'join' ? '' : ' btn-secondary');
|
||||||
|
}
|
||||||
|
<?php if (($_POST['action'] ?? '') === 'join'): ?>
|
||||||
|
switchTab('join');
|
||||||
|
<?php endif; ?>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<?php require __DIR__ . '/footer.php'; ?>
|
||||||
Reference in New Issue
Block a user