Compare commits

...

78 Commits
main ... ai-dev

Author SHA1 Message Date
Flatlogic Bot
99a83d106c V1.4.9 2026-05-07 01:31:16 +00:00
Flatlogic Bot
4e6b10f0bc V1.4.8 2026-05-07 01:11:02 +00:00
Flatlogic Bot
3b2871e0a5 V1.4.7 2026-05-07 00:39:13 +00:00
Flatlogic Bot
3b20de71b5 Autosave: 20260506-233434 2026-05-06 23:34:35 +00:00
Flatlogic Bot
d7e2a86f09 V1.4.6 2026-05-06 22:51:03 +00:00
Flatlogic Bot
58ff3c0818 V1.4.5 2026-04-29 19:31:46 +00:00
Flatlogic Bot
37e7f940e8 V1.4.4 2026-04-29 07:11:28 +00:00
Flatlogic Bot
f74316a661 V1.4.3 2026-04-28 23:04:54 +00:00
Flatlogic Bot
a1559c3861 Autosave: 20260428-225104 2026-04-28 22:51:04 +00:00
Flatlogic Bot
a4cb3a5abc V1.4.2 2026-04-19 21:05:13 +00:00
Flatlogic Bot
3565a88085 V1.4.1 2026-04-19 19:44:04 +00:00
Flatlogic Bot
9063e6830c V1.4.0 2026-04-19 10:02:15 +00:00
Flatlogic Bot
83915bc446 V1.3.9 2026-04-19 09:50:50 +00:00
Flatlogic Bot
d9e76f1b0d Edit index.php via Editor 2026-04-19 09:41:22 +00:00
Flatlogic Bot
03d09df17e Edit index-en.php via Editor 2026-04-19 09:41:03 +00:00
Flatlogic Bot
9329b7b62c V1.3.8 2026-04-19 01:42:39 +00:00
Flatlogic Bot
0619223c4e V1.3.7 2026-04-19 01:31:35 +00:00
Flatlogic Bot
ffbb7a96d6 Autosave: 20260419-012627 2026-04-19 01:26:27 +00:00
Flatlogic Bot
4c879b508b V1.3.6 2026-04-19 01:08:05 +00:00
Flatlogic Bot
40f86c7597 V1.3.5 2026-04-17 22:19:19 +00:00
Flatlogic Bot
2936084d77 V1.3.4 2026-04-16 12:19:08 +00:00
Flatlogic Bot
5b9c8701ca V1.3.3 2026-04-16 12:04:47 +00:00
Flatlogic Bot
6b68ae0708 V1.3.2 2026-04-16 11:54:19 +00:00
Flatlogic Bot
8bf3a87e42 V1.3.1 2026-04-16 10:18:11 +00:00
Flatlogic Bot
be7ca2eca1 V1.3.0 2026-04-16 09:48:29 +00:00
Flatlogic Bot
a472ed9a8f V1.2.7 2026-04-16 09:44:15 +00:00
Flatlogic Bot
5504c68916 V1.2.6 2026-04-16 01:53:04 +00:00
Flatlogic Bot
aadbe1eb46 V1.2.5 2026-04-16 00:55:36 +00:00
Flatlogic Bot
98725a1b7c V1.2.4 2026-04-16 00:49:59 +00:00
Flatlogic Bot
2de33ba813 V1.2.3 2026-04-16 00:23:39 +00:00
Flatlogic Bot
4bf27ecfd4 Autosave: 20260416-001254 2026-04-16 00:12:54 +00:00
Flatlogic Bot
0965f47f80 Autosave: 20260415-231211 2026-04-15 23:12:11 +00:00
Flatlogic Bot
de33ca704e V1.2.2 2026-04-15 18:27:11 +00:00
Flatlogic Bot
dc8e779ba6 V1.2.1 2026-04-15 18:12:05 +00:00
Flatlogic Bot
1659ef93b5 Autosave: 20260415-180938 2026-04-15 18:09:38 +00:00
Flatlogic Bot
ee0bd438e1 Autosave: 20260415-173700 2026-04-15 17:37:00 +00:00
Flatlogic Bot
fe9896701d Autosave: 20260415-145032 2026-04-15 14:50:32 +00:00
Flatlogic Bot
4591e37c7d V1.2.0 2026-04-15 14:39:20 +00:00
Flatlogic Bot
3aa2453da2 V1.1.9 2026-04-15 14:12:47 +00:00
Flatlogic Bot
382882b7e9 Autosave: 20260415-140256 2026-04-15 14:02:57 +00:00
Flatlogic Bot
253188b46d Autosave: 20260415-121623 2026-04-15 12:16:23 +00:00
Flatlogic Bot
4dadf7797d V1.1.8 2026-04-09 17:48:39 +00:00
Flatlogic Bot
0731fe1928 V1.1.7 2026-04-09 17:24:08 +00:00
Flatlogic Bot
1ebb73823c V1.1.6 2026-04-09 17:19:50 +00:00
Flatlogic Bot
d7ffc291ac V1.1.5 2026-04-09 17:17:49 +00:00
Flatlogic Bot
f935801556 Autosave: 20260409-170652 2026-04-09 17:06:52 +00:00
Flatlogic Bot
20d4eb5497 V1.1.4 2026-04-09 12:46:02 +00:00
Flatlogic Bot
1fd504ade9 Autosave: 20260409-114729 2026-04-09 11:47:29 +00:00
Flatlogic Bot
11ac0a6bb5 V1.1.3 2026-04-09 11:35:30 +00:00
Flatlogic Bot
033e89484e Autosave: 20260409-112334 2026-04-09 11:23:34 +00:00
Flatlogic Bot
cab604778c V1.1.2 2026-04-09 09:31:53 +00:00
Flatlogic Bot
ac0f445252 V1.1.1 2026-04-09 02:54:28 +00:00
Flatlogic Bot
2944f9f509 Autosave: 20260409-023612 2026-04-09 02:36:12 +00:00
Flatlogic Bot
a528142c4f Autosave: 20260409-014923 2026-04-09 01:49:23 +00:00
Flatlogic Bot
e191a6916e V1.1.1 2026-04-09 01:39:09 +00:00
Flatlogic Bot
347a95a312 V1.1.0 2026-04-09 01:27:55 +00:00
Flatlogic Bot
fb07ff7d2e Autosave: 20260409-001343 2026-04-09 00:13:43 +00:00
Flatlogic Bot
91b9902c51 Autosave: 20260408-233827 2026-04-08 23:38:27 +00:00
Flatlogic Bot
0c4d8aefa9 Autosave: 20260408-230208 2026-04-08 23:02:08 +00:00
Flatlogic Bot
2145a0438c V1.0.1 2026-04-08 22:02:55 +00:00
Flatlogic Bot
e464a95e35 V1.0.0 2026-04-08 19:11:06 +00:00
Flatlogic Bot
a785e059e9 Autosave: 20260408-184257 2026-04-08 18:42:57 +00:00
Flatlogic Bot
8b445fdaaa V0.9.9 2026-04-08 18:25:53 +00:00
Flatlogic Bot
492e43b1bd V0.9.5 - Bientot la fin 2026-04-08 17:40:29 +00:00
Flatlogic Bot
761c0bd6af Autosave: 20260408-162933 2026-04-08 16:29:33 +00:00
Flatlogic Bot
f8ced7fba3 V0.9.0 2026-04-08 15:34:30 +00:00
Flatlogic Bot
f940a81187 V0.8.7 2026-04-08 10:29:16 +00:00
Flatlogic Bot
1d7d4e1902 V0.8.6 2026-04-08 02:38:27 +00:00
Flatlogic Bot
5e60c40234 V0.8.5 2026-04-08 01:46:27 +00:00
Flatlogic Bot
ce76a05f3d V0.8.2 2026-04-07 23:11:47 +00:00
Flatlogic Bot
c327468bd8 V0.8.1 2026-04-07 22:52:15 +00:00
Flatlogic Bot
a5f381a658 Autosave: 20260407-222735 2026-04-07 22:27:35 +00:00
Flatlogic Bot
9b8c24faa4 V0.7 2026-04-07 22:05:01 +00:00
Flatlogic Bot
ff2b5d34aa V0.6 2026-04-07 21:58:01 +00:00
Flatlogic Bot
ef21d381a0 V0.5 2026-04-07 21:25:05 +00:00
Flatlogic Bot
897ca94a63 V0.4 2026-04-07 21:10:11 +00:00
Flatlogic Bot
385244dfee v0.2 2026-04-07 18:28:23 +00:00
Flatlogic Bot
a26f2122a4 V0.1 2026-04-07 17:47:38 +00:00
52 changed files with 43758 additions and 811 deletions

0
.perm_test_apache Normal file
View File

0
.perm_test_exec Normal file
View File

727
admin.php Normal file
View File

@ -0,0 +1,727 @@
<?php
require_once __DIR__ . '/db/auth.php';
auth_start_session();
auth_bootstrap();
if (!auth_is_logged_in()) {
header('Location: index.php');
exit;
}
$is_admin = auth_is_admin();
$current_role = auth_current_role();
$current_role_label = auth_role_label($current_role);
$flash = auth_flash_get();
$flash_type = $flash['type'] ?? '';
$flash_message = $flash['message'] ?? '';
$edit_cl_auth_id = isset($_GET['edit']) ? (int) $_GET['edit'] : 0;
$edit_cl_auth_user = '';
$edit_cl_auth_right = 'member';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$csrf_token = isset($_POST['csrf_token']) ? (string) $_POST['csrf_token'] : null;
if (!auth_validate_csrf($csrf_token)) {
auth_flash_set('error', 'Jeton CSRF invalide.');
header('Location: admin.php');
exit;
}
$admin_action = (string) ($_POST['admin_action'] ?? '');
if (!$is_admin) {
auth_flash_set('error', 'Seul un administrateur peut gérer les utilisateurs.');
header('Location: admin.php');
exit;
}
if ($admin_action === 'create') {
$submitted_cl_auth_user = trim((string) ($_POST['cl_auth_user'] ?? ''));
$submitted_cl_auth_pass = (string) ($_POST['cl_auth_pass'] ?? '');
$submitted_cl_auth_right = (string) ($_POST['cl_auth_right'] ?? 'member');
if ($submitted_cl_auth_user === '' || $submitted_cl_auth_pass === '') {
auth_flash_set('error', 'Le login et le mot de passe sont obligatoires.');
header('Location: admin.php');
exit;
}
if (!in_array($submitted_cl_auth_right, auth_valid_roles(), true)) {
auth_flash_set('error', 'Droit utilisateur invalide.');
header('Location: admin.php');
exit;
}
$stmt_duplicate_user = db()->prepare('SELECT COUNT(*) FROM tbl_auth WHERE cl_auth_user = :cl_auth_user');
$stmt_duplicate_user->execute([
'cl_auth_user' => $submitted_cl_auth_user,
]);
$cl_auth_user_total = (int) $stmt_duplicate_user->fetchColumn();
if ($cl_auth_user_total > 0) {
auth_flash_set('error', 'Ce login existe déjà.');
header('Location: admin.php');
exit;
}
$cl_auth_user = $submitted_cl_auth_user;
$cl_auth_pass = password_hash($submitted_cl_auth_pass, PASSWORD_DEFAULT);
$cl_auth_right = $submitted_cl_auth_right;
$stmt_create_user = db()->prepare(
'INSERT INTO tbl_auth (cl_auth_user, cl_auth_pass, cl_auth_right) VALUES (:cl_auth_user, :cl_auth_pass, :cl_auth_right)'
);
$stmt_create_user->execute([
'cl_auth_user' => $cl_auth_user,
'cl_auth_pass' => $cl_auth_pass,
'cl_auth_right' => $cl_auth_right,
]);
auth_flash_set('success', 'Compte créé avec succès.');
header('Location: admin.php');
exit;
}
if ($admin_action === 'update') {
$cl_auth_id = (int) ($_POST['cl_auth_id'] ?? 0);
$submitted_cl_auth_user = trim((string) ($_POST['cl_auth_user'] ?? ''));
$submitted_cl_auth_pass = (string) ($_POST['cl_auth_pass'] ?? '');
$submitted_cl_auth_right = (string) ($_POST['cl_auth_right'] ?? 'member');
if ($cl_auth_id <= 0 || $submitted_cl_auth_user === '') {
auth_flash_set('error', 'Données de modification invalides.');
header('Location: admin.php');
exit;
}
if (!in_array($submitted_cl_auth_right, auth_valid_roles(), true)) {
auth_flash_set('error', 'Droit utilisateur invalide.');
header('Location: admin.php?edit=' . $cl_auth_id);
exit;
}
$stmt_tbl_auth = db()->prepare('SELECT cl_auth_id, cl_auth_user, cl_auth_pass, cl_auth_right FROM tbl_auth WHERE cl_auth_id = :cl_auth_id LIMIT 1');
$stmt_tbl_auth->execute([
'cl_auth_id' => $cl_auth_id,
]);
$tbl_auth = $stmt_tbl_auth->fetch();
if (!$tbl_auth) {
auth_flash_set('error', 'Utilisateur introuvable.');
header('Location: admin.php');
exit;
}
$current_cl_auth_id = (int) $tbl_auth['cl_auth_id'];
$current_cl_auth_user = (string) $tbl_auth['cl_auth_user'];
$current_cl_auth_pass = (string) $tbl_auth['cl_auth_pass'];
$current_cl_auth_right = (string) $tbl_auth['cl_auth_right'];
unset($current_cl_auth_id, $current_cl_auth_user);
$stmt_duplicate_user = db()->prepare(
'SELECT COUNT(*) FROM tbl_auth WHERE cl_auth_user = :cl_auth_user AND cl_auth_id <> :cl_auth_id'
);
$stmt_duplicate_user->execute([
'cl_auth_user' => $submitted_cl_auth_user,
'cl_auth_id' => $cl_auth_id,
]);
$cl_auth_user_total = (int) $stmt_duplicate_user->fetchColumn();
if ($cl_auth_user_total > 0) {
auth_flash_set('error', 'Ce login existe déjà.');
header('Location: admin.php?edit=' . $cl_auth_id);
exit;
}
if ($current_cl_auth_right === 'admin' && $submitted_cl_auth_right !== 'admin') {
$stmt_admin_total = db()->query("SELECT COUNT(*) FROM tbl_auth WHERE cl_auth_right = 'admin'");
$cl_auth_admin_total = (int) $stmt_admin_total->fetchColumn();
if ($cl_auth_admin_total <= 1) {
auth_flash_set('error', 'Impossible de rétrograder le dernier administrateur.');
header('Location: admin.php?edit=' . $cl_auth_id);
exit;
}
}
$cl_auth_user = $submitted_cl_auth_user;
$cl_auth_right = $submitted_cl_auth_right;
$cl_auth_pass = $current_cl_auth_pass;
if ($submitted_cl_auth_pass !== '') {
$cl_auth_pass = password_hash($submitted_cl_auth_pass, PASSWORD_DEFAULT);
}
$stmt_update_user = db()->prepare(
'UPDATE tbl_auth
SET cl_auth_user = :cl_auth_user,
cl_auth_pass = :cl_auth_pass,
cl_auth_right = :cl_auth_right
WHERE cl_auth_id = :cl_auth_id'
);
$stmt_update_user->execute([
'cl_auth_user' => $cl_auth_user,
'cl_auth_pass' => $cl_auth_pass,
'cl_auth_right' => $cl_auth_right,
'cl_auth_id' => $cl_auth_id,
]);
if (isset($_SESSION['user']) && $_SESSION['user'] === $tbl_auth['cl_auth_user']) {
$_SESSION['user'] = $cl_auth_user;
$_SESSION['role'] = $cl_auth_right;
}
auth_flash_set('success', 'Compte modifié avec succès.');
header('Location: admin.php');
exit;
}
if ($admin_action === 'delete') {
$cl_auth_id = (int) ($_POST['cl_auth_id'] ?? 0);
if ($cl_auth_id <= 0) {
auth_flash_set('error', 'Suppression impossible.');
header('Location: admin.php');
exit;
}
$stmt_tbl_auth = db()->prepare('SELECT cl_auth_id, cl_auth_user, cl_auth_pass, cl_auth_right FROM tbl_auth WHERE cl_auth_id = :cl_auth_id LIMIT 1');
$stmt_tbl_auth->execute([
'cl_auth_id' => $cl_auth_id,
]);
$tbl_auth = $stmt_tbl_auth->fetch();
if (!$tbl_auth) {
auth_flash_set('error', 'Utilisateur introuvable.');
header('Location: admin.php');
exit;
}
$cl_auth_user = (string) $tbl_auth['cl_auth_user'];
$cl_auth_pass = (string) $tbl_auth['cl_auth_pass'];
$cl_auth_right = (string) $tbl_auth['cl_auth_right'];
unset($cl_auth_pass);
if ($cl_auth_right === 'admin') {
$stmt_admin_total = db()->query("SELECT COUNT(*) FROM tbl_auth WHERE cl_auth_right = 'admin'");
$cl_auth_admin_total = (int) $stmt_admin_total->fetchColumn();
if ($cl_auth_admin_total <= 1) {
auth_flash_set('error', 'Impossible de supprimer le dernier administrateur.');
header('Location: admin.php');
exit;
}
}
$stmt_delete_user = db()->prepare('DELETE FROM tbl_auth WHERE cl_auth_id = :cl_auth_id');
$stmt_delete_user->execute([
'cl_auth_id' => $cl_auth_id,
]);
if (isset($_SESSION['user']) && $_SESSION['user'] === $cl_auth_user) {
header('Location: logout.php');
exit;
}
auth_flash_set('success', 'Compte supprimé avec succès.');
header('Location: admin.php');
exit;
}
}
if ($is_admin && $edit_cl_auth_id > 0) {
$stmt_edit_user = db()->prepare('SELECT cl_auth_id, cl_auth_user, cl_auth_pass, cl_auth_right FROM tbl_auth WHERE cl_auth_id = :cl_auth_id LIMIT 1');
$stmt_edit_user->execute([
'cl_auth_id' => $edit_cl_auth_id,
]);
$tbl_auth = $stmt_edit_user->fetch();
if ($tbl_auth) {
$edit_cl_auth_id = (int) $tbl_auth['cl_auth_id'];
$edit_cl_auth_user = (string) $tbl_auth['cl_auth_user'];
$edit_cl_auth_pass = (string) $tbl_auth['cl_auth_pass'];
$edit_cl_auth_right = (string) $tbl_auth['cl_auth_right'];
unset($edit_cl_auth_pass);
} else {
$edit_cl_auth_id = 0;
auth_flash_set('error', 'Utilisateur introuvable.');
header('Location: admin.php');
exit;
}
}
$tbl_auth_all = [];
if ($is_admin) {
$stmt_users = db()->query('SELECT cl_auth_id, cl_auth_user, cl_auth_pass, cl_auth_right FROM tbl_auth ORDER BY cl_auth_user ASC');
$tbl_auth_all = $stmt_users->fetchAll();
}
$user_accessible_items = [];
if (!$is_admin) {
foreach (auth_navigation_items() as $item) {
$file = (string) ($item['file'] ?? '');
$label = (string) ($item['label'] ?? $file);
$admin_only = !empty($item['admin_only']);
if ($admin_only) {
continue;
}
if (auth_user_can_access_page($file, $label)) {
$user_accessible_items[] = [
'file' => $file,
'label' => $label,
];
}
}
}
$csrf_token = auth_csrf_token();
[$default_admin_user, $default_admin_password] = auth_default_admin_credentials();
$current_session_user = isset($_SESSION['user']) ? (string) $_SESSION['user'] : '';
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Administration Sécure | R.E.A.C.T.</title>
<link rel="stylesheet" type="text/css" href="css/styles.css">
<link rel="stylesheet" type="text/css" href="css/default.css">
<style>
:root {
--primary: #a29b78;
--primary-glow: rgba(162, 155, 120, 0.4);
--bg-dark: #080a0f;
--card-bg: rgba(20, 24, 33, 0.85);
--border-glow: rgba(162, 155, 120, 0.25);
--danger: #ff4d4d;
--success: #00ff88;
}
body {
background: radial-gradient(circle at top right, #1a1f2e, var(--bg-dark));
background-attachment: fixed;
color: #e0e0e0;
font-family: 'Electrolize', sans-serif;
margin: 0;
overflow-x: hidden;
min-height: 100vh;
}
.admin-layout {
display: flex;
flex-direction: column;
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
animation: fadeIn 0.6s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.admin-topbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 2rem;
background: var(--card-bg);
backdrop-filter: blur(10px);
border: 1px solid var(--border-glow);
border-radius: 12px;
margin-bottom: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.topbar-info h1 {
margin: 0;
font-size: 1.5rem;
letter-spacing: 2px;
text-transform: uppercase;
background: linear-gradient(90deg, #fff, var(--primary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.topbar-info p {
margin: 0.25rem 0 0;
font-size: 0.85rem;
color: var(--primary);
opacity: 0.8;
}
.topbar-actions {
display: flex;
gap: 1rem;
flex-wrap: wrap;
align-items: center;
}
.session-user {
opacity: 0.85;
}
.topbar-actions {
display: flex;
gap: 1rem;
}
.btn-modern {
padding: 0.6rem 1.2rem;
border: 1px solid var(--primary);
background: transparent;
color: #fff;
font-family: 'Electrolize', sans-serif;
font-size: 0.9rem;
text-transform: uppercase;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 4px;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
}
.btn-modern:hover {
background: var(--primary);
color: var(--bg-dark);
box-shadow: 0 0 15px var(--primary-glow);
}
.btn-modern.danger {
border-color: var(--danger);
color: var(--danger);
}
.btn-modern.danger:hover {
background: var(--danger);
color: #fff;
box-shadow: 0 0 15px rgba(255, 77, 77, 0.3);
}
.nav-tabs { display: flex; gap: 1rem; margin-bottom: 2rem; border-bottom: 1px solid var(--border-glow); padding-bottom: 1rem; }
.nav-tabs a { text-decoration: none; color: #888; text-transform: uppercase; font-size: 0.9rem; transition: color 0.3s; }
.nav-tabs a:hover, .nav-tabs a.active { color: var(--primary); }
.admin-content {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 2rem;
}
@media (max-width: 1024px) {
.admin-content { grid-template-columns: 1fr; }
}
.glass-card {
background: var(--card-bg);
backdrop-filter: blur(12px);
border: 1px solid var(--border-glow);
border-radius: 12px;
padding: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
}
.glass-card h2 {
margin-top: 0;
margin-bottom: 1.5rem;
font-size: 1.25rem;
color: var(--primary);
border-bottom: 1px solid var(--border-glow);
padding-bottom: 0.75rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-size: 0.85rem;
color: #aaa;
text-transform: uppercase;
}
.form-control {
width: 100%;
padding: 0.8rem 1rem;
background: rgba(0, 0, 0, 0.3);
border: 1px solid #444;
border-radius: 4px;
color: #fff;
font-family: 'Electrolize', sans-serif;
transition: border-color 0.3s;
}
.form-control:focus {
outline: none;
border-color: var(--primary);
background: rgba(0, 0, 0, 0.5);
}
select.form-control {
background: #353b45;
color: #fff;
border-color: #565d68;
color-scheme: dark;
}
select.form-control:focus {
background: #3d444f;
color: #fff;
}
select.form-control option {
background: #353b45;
color: #fff;
}
select.form-control option:checked {
background: #4a5260;
color: #fff;
}
.modern-table {
width: 100%;
border-collapse: separate;
border-spacing: 0 8px;
}
.modern-table th {
text-align: left;
padding: 1rem;
font-size: 0.8rem;
text-transform: uppercase;
color: var(--primary);
opacity: 0.7;
}
.modern-table td {
padding: 1rem;
background: rgba(255, 255, 255, 0.03);
border-top: 1px solid rgba(255, 255, 255, 0.05);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.modern-table td:first-child {
border-left: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 8px 0 0 8px;
}
.modern-table td:last-child {
border-right: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 0 8px 8px 0;
}
.modern-table tr:hover td {
background: rgba(162, 155, 120, 0.05);
}
.badge {
padding: 0.25rem 0.6rem;
border-radius: 4px;
font-size: 0.75rem;
text-transform: uppercase;
}
.badge-admin { background: rgba(162, 155, 120, 0.2); color: var(--primary); border: 1px solid var(--primary); }
.badge-moderator { background: rgba(74, 144, 226, 0.16); color: #8fc7ff; border: 1px solid rgba(143, 199, 255, 0.6); }
.badge-member { background: rgba(255, 255, 255, 0.1); color: #ccc; border: 1px solid #555; }
.flash {
padding: 1rem 1.5rem;
border-radius: 8px;
margin-bottom: 1.5rem;
font-size: 0.9rem;
border-left: 4px solid var(--primary);
background: rgba(162, 155, 120, 0.1);
}
.flash.error { border-color: var(--danger); background: rgba(255, 77, 77, 0.1); color: #ffbaba; }
.flash.success { border-color: var(--success); background: rgba(0, 255, 136, 0.1); color: #baffda; }
.row-actions {
display: flex;
gap: 0.5rem;
}
.btn-mini {
padding: 0.3rem 0.6rem;
font-size: 0.75rem;
}
.user-id {
font-family: monospace;
color: var(--primary);
font-weight: bold;
}
.empty-state {
padding: 2rem;
text-align: center;
color: #666;
font-style: italic;
}
</style>
</head>
<body>
<div class="admin-layout">
<header class="admin-topbar">
<div class="topbar-info">
<h1>R.E.A.C.T. Core Admin</h1>
<p>Niveau d'accès : <strong><?php echo $is_admin ? 'Administrateur' : 'Membre'; ?></strong></p>
</div>
<div class="topbar-actions">
<span class="session-user">Connecté : <strong><?php echo htmlspecialchars($current_session_user, ENT_QUOTES, 'UTF-8'); ?></strong></span>
<a href="index.php" class="btn-modern">Retour au site</a>
<a href="logout.php" class="btn-modern danger">Déconnexion</a>
</div>
</header>
<?php if ($is_admin): ?>
<?php echo auth_render_app_nav('admin.php'); ?>
<?php else: ?>
<nav class="nav-tabs">
<a href="admin.php" class="active">Zone admin</a>
<?php foreach ($member_accessible_items as $item): ?>
<a href="<?php echo htmlspecialchars($item['file'], ENT_QUOTES, 'UTF-8'); ?>"><?php echo htmlspecialchars($item['label'], ENT_QUOTES, 'UTF-8'); ?></a>
<?php endforeach; ?>
</nav>
<?php endif; ?>
<?php if ($flash_message !== ''): ?>
<div class="flash <?php echo htmlspecialchars($flash_type, ENT_QUOTES, 'UTF-8'); ?>">
<?php echo htmlspecialchars($flash_message, ENT_QUOTES, 'UTF-8'); ?>
</div>
<?php endif; ?>
<?php if ($is_admin && $default_admin_user === 'admin'): ?>
<div class="flash">
<strong style="color: var(--primary);">Sécurité critique :</strong> Les identifiants par défaut sont actifs.
(<code><?php echo htmlspecialchars($default_admin_user, ENT_QUOTES, 'UTF-8'); ?></code> / <code><?php echo htmlspecialchars($default_admin_password, ENT_QUOTES, 'UTF-8'); ?></code>)
<br><small>Veuillez modifier ces accès dès maintenant.</small>
</div>
<?php endif; ?>
<?php if ($is_admin): ?>
<main class="admin-content">
<!-- Form Card -->
<section class="glass-card">
<h2><?php echo $edit_cl_auth_id > 0 ? 'Mise à jour sujet' : 'Nouveau sujet'; ?></h2>
<form method="post" action="admin.php<?php echo $edit_cl_auth_id > 0 ? '?edit=' . $edit_cl_auth_id : ''; ?>">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="admin_action" value="<?php echo $edit_cl_auth_id > 0 ? 'update' : 'create'; ?>">
<?php if ($edit_cl_auth_id > 0): ?>
<input type="hidden" name="cl_auth_id" value="<?php echo $edit_cl_auth_id; ?>">
<?php endif; ?>
<div class="form-group">
<label for="cl_auth_user">Identifiant Système</label>
<input class="form-control" id="cl_auth_user" name="cl_auth_user" type="text" required value="<?php echo htmlspecialchars($edit_cl_auth_user, ENT_QUOTES, 'UTF-8'); ?>" placeholder="Nom d'utilisateur">
</div>
<div class="form-group">
<label for="cl_auth_pass"><?php echo $edit_cl_auth_id > 0 ? 'Nouveau Secret (vide = inchangé)' : 'Secret d\'accès'; ?></label>
<input class="form-control" id="cl_auth_pass" name="cl_auth_pass" type="password" <?php echo $edit_cl_auth_id > 0 ? '' : 'required'; ?> placeholder="••••••••">
</div>
<div class="form-group">
<label for="cl_auth_right">Niveau d'accréditation</label>
<select class="form-control" id="cl_auth_right" name="cl_auth_right">
<option value="admin" <?php echo $edit_cl_auth_right === 'admin' ? 'selected' : ''; ?>>Administrateur</option>
<option value="moderator" <?php echo $edit_cl_auth_right === 'moderator' ? 'selected' : ''; ?>>Modérateur</option>
<option value="member" <?php echo $edit_cl_auth_right === 'member' ? 'selected' : ''; ?>>Membre</option>
</select>
</div>
<div style="display: flex; gap: 10px; margin-top: 2rem;">
<button class="btn-modern" style="flex: 2;" type="submit">
<?php echo $edit_cl_auth_id > 0 ? 'Appliquer' : 'Initialiser'; ?>
</button>
<?php if ($edit_cl_auth_id > 0): ?>
<a class="btn-modern danger" style="flex: 1;" href="admin.php">Annuler</a>
<?php endif; ?>
</div>
</form>
</section>
<!-- Table Card -->
<section class="glass-card">
<h2>Base de données sujets</h2>
<div style="overflow-x: auto;">
<table class="modern-table">
<thead>
<tr>
<th>UID</th>
<th>Sujet</th>
<th>Accréditation</th>
<th style="text-align: right;">Opérations</th>
</tr>
</thead>
<tbody>
<?php if (empty($tbl_auth_all)): ?>
<tr>
<td colspan="4" class="empty-state">Aucun sujet détecté dans la base.</td>
</tr>
<?php else: ?>
<?php foreach ($tbl_auth_all as $tbl_auth): ?>
<?php
$cl_auth_id = (int) $tbl_auth['cl_auth_id'];
$cl_auth_user = (string) $tbl_auth['cl_auth_user'];
$cl_auth_right = (string) $tbl_auth['cl_auth_right'];
?>
<tr>
<td><span class="user-id">#<?php echo sprintf('%03d', $cl_auth_id); ?></span></td>
<td><strong><?php echo htmlspecialchars($cl_auth_user, ENT_QUOTES, 'UTF-8'); ?></strong></td>
<td>
<span class="badge <?php echo $cl_auth_right === 'admin' ? 'badge-admin' : ($cl_auth_right === 'moderator' ? 'badge-moderator' : 'badge-member'); ?>">
<?php echo htmlspecialchars(auth_role_label($cl_auth_right), ENT_QUOTES, 'UTF-8'); ?>
</span>
</td>
<td>
<div class="row-actions" style="justify-content: flex-end;">
<a class="btn-modern btn-mini" href="admin.php?edit=<?php echo $cl_auth_id; ?>">Editer</a>
<form method="post" action="admin.php" onsubmit="return confirm('Terminer ce sujet ? Cette action est irréversible.');">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="admin_action" value="delete">
<input type="hidden" name="cl_auth_id" value="<?php echo $cl_auth_id; ?>">
<button class="btn-modern btn-mini danger" type="submit">X</button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</section>
</main>
<?php else: ?>
<main class="admin-content" style="grid-template-columns: 1fr;">
<section class="glass-card">
<h2>Zone <?php echo htmlspecialchars($current_role_label, ENT_QUOTES, 'UTF-8'); ?></h2>
<p>Vous êtes bien entré dans la zone admin avec un compte <strong><?php echo htmlspecialchars($current_role_label, ENT_QUOTES, 'UTF-8'); ?></strong>.</p>
<p>La gestion des utilisateurs reste réservée aux administrateurs, mais vous pouvez utiliser ci-dessous les pages ouvertes à votre niveau d'autorisation.</p>
<?php if (empty($user_accessible_items)): ?>
<div class="empty-state">Aucune page ne vous a encore été attribuée pour ce rôle.</div>
<?php else: ?>
<div class="row-actions" style="flex-wrap: wrap; gap: 12px; margin-top: 1rem;">
<?php foreach ($user_accessible_items as $item): ?>
<a class="btn-modern" href="<?php echo htmlspecialchars($item['file'], ENT_QUOTES, 'UTF-8'); ?>"><?php echo htmlspecialchars($item['label'], ENT_QUOTES, 'UTF-8'); ?></a>
<?php endforeach; ?>
</div>
<?php endif; ?>
</section>
</main>
<?php endif; ?>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 740 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 745 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 630 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 746 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 766 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 839 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 754 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

View File

@ -1,151 +1,236 @@
/* General styles for the modal */
/*
Styles for the html/body for special modal where we want 3d effects
Note that we need a container wrapping all content on the page for the
perspective effects (not including the modals and the overlay).
*/
.md-perspective,
.md-perspective body {
height: 100%;
overflow: hidden;
}
.md-perspective body {
background: #d4c6a1;
-webkit-perspective: 600px;
-moz-perspective: 600px;
perspective: 600px;
}
.container {
min-height: 100%;
}
.md-modal {
position: fixed;
top: 50%;
left: 50%;
width: 50%;
max-width: 1400px;
min-width: 1200px;
height: auto;
z-index: 2000;
visibility: hidden;
-webkit-backface-visibility: hidden;
-moz-backface-visibility: hidden;
backface-visibility: hidden;
-webkit-transform: translateX(-50%) translateY(-50%);
-moz-transform: translateX(-50%) translateY(-50%);
-ms-transform: translateX(-50%) translateY(-50%);
transform: translateX(-50%) translateY(-50%);
}
.md-show {
visibility: visible;
}
.md-overlay {
position: fixed;
width: 100%;
height: 100%;
visibility: hidden;
top: 0;
left: 0;
z-index: 1000;
opacity: 0;
background: rgba(145,120,30,0.8);
-webkit-transition: all 0.3s;
-moz-transition: all 0.3s;
transition: all 0.3s;
}
.md-show ~ .md-overlay {
opacity: 1;
visibility: visible;
}
/* Content styles */
.md-content {
color: #fff;
background: #544c37;
position: relative;
border-radius: 3px;
margin: 0 auto;
}
.md-content h3 {
margin: 0;
padding: 0.4em;
text-align: center;
font-size: 2.4em;
font-weight: 300;
opacity: 0.8;
background: rgba(0,0,0,0.1);
border-radius: 3px 3px 0 0;
}
.md-content > div {
padding: 15px 40px 30px;
margin: 0;
font-weight: 300;
font-size: 1.15em;
}
.md-content > div p {
margin: 0;
padding: 10px 0;
text-align: justify;
}
.md-content > div ul {
margin: 0;
padding: 0 0 30px 20px;
}
.md-content > div ul li {
padding: 5px 0;
}
.md-content button {
display: block;
margin: 0 auto;
font-size: 0.8em;
}
/* Individual modal styles with animations/transitions */
/* Effect 1: Fade in and scale up */
.md-effect-1 .md-content {
-webkit-transform: scale(0.7);
-moz-transform: scale(0.7);
-ms-transform: scale(0.7);
transform: scale(0.7);
opacity: 0;
-webkit-transition: all 0.3s;
-moz-transition: all 0.3s;
transition: all 0.3s;
}
.md-show.md-effect-1 .md-content {
-webkit-transform: scale(1);
-moz-transform: scale(1);
-ms-transform: scale(1);
transform: scale(1);
opacity: 1;
}
@-webkit-keyframes slit {
50% { -webkit-transform: translateZ(-250px) rotateY(89deg); opacity: .5; -webkit-animation-timing-function: ease-out;}
100% { -webkit-transform: translateZ(0) rotateY(0deg); opacity: 1; }
}
@-moz-keyframes slit {
50% { -moz-transform: translateZ(-250px) rotateY(89deg); opacity: .5; -moz-animation-timing-function: ease-out;}
100% { -moz-transform: translateZ(0) rotateY(0deg); opacity: 1; }
}
@keyframes slit {
50% { transform: translateZ(-250px) rotateY(89deg); opacity: 1; animation-timing-function: ease-in;}
100% { transform: translateZ(0) rotateY(0deg); opacity: 1; }
}
/* General styles for the modal */
/*
Styles for the html/body for special modal where we want 3d effects
Note that we need a container wrapping all content on the page for the
perspective effects (not including the modals and the overlay).
*/
.md-perspective,
.md-perspective body {
height: 100%;
overflow: hidden;
}
.md-perspective body {
background: #d4c6a1;
-webkit-perspective: 600px;
-moz-perspective: 600px;
perspective: 600px;
}
.container {
min-height: 100%;
}
.md-modal {
position: fixed;
top: 50%;
left: 50%;
width: min(92vw, 1100px);
max-width: calc(100vw - 32px);
min-width: 320px;
max-height: calc(100vh - 32px);
height: auto;
overflow-y: auto;
z-index: 2000;
visibility: hidden;
-webkit-backface-visibility: hidden;
-moz-backface-visibility: hidden;
backface-visibility: hidden;
-webkit-transform: translateX(-50%) translateY(-50%);
-moz-transform: translateX(-50%) translateY(-50%);
-ms-transform: translateX(-50%) translateY(-50%);
transform: translateX(-50%) translateY(-50%);
}
.md-show {
visibility: visible;
}
.md-overlay {
position: fixed;
width: 100%;
height: 100%;
visibility: hidden;
top: 0;
left: 0;
z-index: 1000;
opacity: 0;
background: rgba(145,120,30,0.8);
-webkit-transition: all 0.3s;
-moz-transition: all 0.3s;
transition: all 0.3s;
}
.md-show ~ .md-overlay {
opacity: 1;
visibility: visible;
}
/* Content styles */
.md-content {
color: #fff;
background: #544c37;
position: relative;
border-radius: 3px;
margin: 0 auto;
max-width: 100%;
overflow: hidden;
}
.md-content img {
max-width: 100%;
height: auto;
}
.md-content h3 {
margin: 0;
padding: 0.4em;
text-align: center;
font-size: 2.4em;
font-weight: 300;
opacity: 0.8;
background: rgba(0,0,0,0.1);
border-radius: 3px 3px 0 0;
}
.md-content > div {
padding: 15px 40px 30px;
margin: 0;
font-weight: 300;
font-size: 1.15em;
}
.md-content > div p {
margin: 0;
padding: 10px 0;
text-align: justify;
}
.md-content > div ul {
margin: 0;
padding: 0 0 30px 20px;
}
.md-content > div ul li {
padding: 5px 0;
}
.md-content button {
display: block;
margin: 0 auto;
font-size: 0.8em;
}
/* Individual modal styles with animations/transitions */
/* Effect 1: Fade in and scale up */
.md-effect-1 .md-content {
-webkit-transform: scale(0.7);
-moz-transform: scale(0.7);
-ms-transform: scale(0.7);
transform: scale(0.7);
opacity: 0;
-webkit-transition: all 0.3s;
-moz-transition: all 0.3s;
transition: all 0.3s;
}
.md-show.md-effect-1 .md-content {
-webkit-transform: scale(1);
-moz-transform: scale(1);
-ms-transform: scale(1);
transform: scale(1);
opacity: 1;
}
@-webkit-keyframes slit {
50% { -webkit-transform: translateZ(-250px) rotateY(89deg); opacity: .5; -webkit-animation-timing-function: ease-out;}
100% { -webkit-transform: translateZ(0) rotateY(0deg); opacity: 1; }
}
@-moz-keyframes slit {
50% { -moz-transform: translateZ(-250px) rotateY(89deg); opacity: .5; -moz-animation-timing-function: ease-out;}
100% { -moz-transform: translateZ(0) rotateY(0deg); opacity: 1; }
}
@keyframes slit {
50% { transform: translateZ(-250px) rotateY(89deg); opacity: 1; animation-timing-function: ease-in;}
100% { transform: translateZ(0) rotateY(0deg); opacity: 1; }
}
@media (max-width: 1280px) {
.md-modal {
width: min(94vw, 960px);
min-width: 0;
}
}
@media (max-width: 900px) {
.md-content > div img.float-left,
.md-content > div img.float-right {
float: none;
display: block;
margin: 0 auto 16px;
max-width: min(100%, 260px);
}
}
@media (max-width: 768px) {
.md-modal {
width: calc(100vw - 24px);
max-width: calc(100vw - 24px);
max-height: calc(100vh - 24px);
}
.md-content h3 {
position: relative;
padding: 0.65em 3.4rem;
font-size: 1.5em;
line-height: 1.3;
}
.md-content > div {
padding: 16px 18px 20px;
font-size: 1em;
}
.md-content h3 img.float-left {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
float: none;
}
.md-content h3 .frame-icon-close.float-right {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
float: none;
}
}
@media (max-width: 480px) {
.md-modal {
width: calc(100vw - 16px);
max-width: calc(100vw - 16px);
min-width: 0;
max-height: calc(100vh - 16px);
}
.md-content h3 {
padding: 0.85em 2.8rem;
font-size: 1.15em;
}
.md-content > div {
padding: 14px 12px 18px;
font-size: 0.95em;
}
.md-content > div p {
text-align: left;
}
}

View File

@ -1,312 +1,536 @@
@font-face {
font-family: 'Electrolize';
src: url('../fonts/Electrolize-Regular.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: Electrolize, Arial, sans-serif;
}
html, body {
height: 100%;
}
body {
background: #1a1a1a;
color: white;
min-height: 100vh;
}
a {
color: #ffffff;
text-decoration: none;
}
a:hover {
color: #a29b78 !important;
text-decoration: none;
}
.footer {
position: absolute;
bottom: 0px;
left: 50%;
transform: translate(-50%, -50%);
width: -50%;
height: 50px;
}
/* Utilitaires texte */
.txt-s05 { font-size: 5px; }
.txt-s10 { font-size: 10px; }
.txt-s12 { font-size: 12px; }
.txt-s15 { font-size: 15px; }
.txt-s20 { font-size: 20px; }
.txt-s22 { font-size: 22px; }
.txt-s25 { font-size: 25px; }
.txt-s40 { font-size: 40px; }
.padding5 { padding: 5px; }
.padding15 { padding: 15px; }
.padding25 { padding: 25px; }
.padding50 { padding: 50px; }
.padding-left50 { padding-left: 50px !important; }
.txt-bold { font-weight: bold; }
.txt-italic { font-style: italic; }
.txt-justify { text-align: justify; text-justify: inter-word; }
.txt-center { text-align: center !important; }
.txt-center input { display: inline-block !important; text-align: center !important; }
.txt-underline { text-decoration: underline; }
.float-right { float: right; }
.float-left { float: left; }
.txt-or { color: #a29b78; }
.txt-jaune { color: #fcba03; }
.v-hidden { visibility: hidden; }
.small-caps { font-variant-caps: small-caps; }
.ul-style {
line-height: 30px;
/* padding-left: 50px !important; */
/* border-left: 4px solid #a29b78; */
}
/* Vidéo plein écran en arrière-plan */
.video-container {
width: 100vw;
height: 100vh;
position: fixed;
inset: 0;
z-index: -5; /* derrière tout */
overflow: hidden;
}
.video-container iframe {
position: absolute;
top: 50%;
left: 50%;
width: 100vw;
height: 100vh;
transform: translate(-50%, -50%);
border: 0;
pointer-events: none; /* la vidéo nintercepte pas les clics */
}
/* Ajustements pour conserver le recadrage 16/9 sans bandes */
@media (min-aspect-ratio: 16/9) {
.video-container iframe { height: 56.25vw; }
}
@media (max-aspect-ratio: 16/9) {
.video-container iframe { width: 177.78vh; }
}
/* Voile noir par-dessus la vidéo */
.black-filter {
background: rgba(0, 0, 0, 0.7);
position: absolute;
inset: 0;
z-index: -1;
}
/* Conteneur principal (si besoin de couches au-dessus) */
.container {
width: 100%;
min-height: 100vh;
display: flex;
flex-direction: column;
position: relative;
z-index: 0; /* au-dessus de la vidéo */
}
/* Logos centrés */
.center-page-bops {
position: absolute;
top: 35%;
left: 50%;
transform: translate(-50%, -50%);
background-repeat: no-repeat;
background-position: center center;
background-size: auto;
z-index: 2;
width: 600px;
height: 600px;
}
.center-page-infos {
position: absolute;
top: 0%;
left: 50%;
transform: translate(-50%, -50%);
background-repeat: no-repeat;
background-position: center center;
background-size: auto;
z-index: 2;
width: 600px;
height: 200px;
}
.assets-div-menu {
position: fixed;
top: 25px;
left: 25px;
transform: translate(0px, 0px);
background-repeat: no-repeat;
background-position: center center;
background-size: auto;
background-color: rgb(0 0 0 / 50%);
z-index: 100;
width: 300px;
display: flex; /* Active le mode Flexbox */
flex-direction: column; /* Aligne les liens les uns sous les autres */
align-items: center; /* Centre verticalement */
gap: 2px;
justify-content: center;
border: solid 3px rgb(155 145 60 / 25%);
border-radius: 10px;
padding: 5px 0px 5px 0px;
}
.connexion-div-menu {
position: fixed;
top: 25px;
right: 25px;
transform: translate(0px, 0px);
background-repeat: no-repeat;
background-position: center center;
background-size: auto;
background-color: rgb(0 0 0 / 50%);
z-index: 100;
width: 300px;
height: 35px;
display: flex; /* Active le mode Flexbox */
align-items: center; /* Centre verticalement */
justify-content: center;
border: solid 3px rgb(155 145 60 / 25%);
border-radius: 10px;
}
.center-div-menu {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-repeat: no-repeat;
background-position: center center;
background-size: auto;
z-index: 3;
width: 1050px;
height: 80px;
text-align: center;
}
.center-div-menu-flex {
display: flex; /* Active l'affichage en ligne */
justify-content: center; /* Centre les items horizontalement */
align-items: center; /* Centre verticalement */
gap: 10px; /* Espace entre les divs (modifiable) */
}
.center-div-menu .menu-item {
min-height: 40px; /* hauteur de chaque div */
/* background: #ccc; /* juste pour visualiser, tu peux retirer */
/* border-radius: 5px; */
padding: 5px;
}
.center-div-menu .menu-item:hover {
min-height: 40px; /* hauteur de chaque div */
/* background: #ccc; /* juste pour visualiser, tu peux retirer */
/* border-radius: 5px; */
border-bottom: 4px solid #a29b78;
padding: 5px;
}
.center-div-menu .menu-item:hover a {
color: #a29b78 !important;
}
.menu-item-we {
min-width: 20px; /* largeur de chaque div (modifiable) */
min-height: 40px; /* hauteur de chaque div */
padding: 5px;
}
.center-page-bops {
background-image: url("../img/logo-org.png");
z-index: 2;
animation: spin-horizontal 5s linear infinite;
}
/* Animation de rotation horizontale infinie */
@keyframes spin-horizontal {
0% { transform: translate(-50%, -50%) rotateY(0deg); }
50% { transform: translate(-50%, -50%) rotateY(0deg); }
100% { transform: translate(-50%, -50%) rotateY(360deg); }
}
/* Contrôles audio en haut à droite */
.audio-controls {
position: fixed;
top: 15px;
right: 15px;
display: flex;
align-items: center;
gap: 8px;
z-index: 10;
background: rgba(0, 0, 0, 0.6);
padding: 6px 10px;
border-radius: 8px;
backdrop-filter: blur(2px);
}
.audio-controls button {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
color: white;
line-height: 1;
}
.audio-controls input[type="range"] {
width: 120px;
cursor: pointer;
accent-color: #fcba03; /* facultatif si supporté */
}
.vol-label {
font-size: 12px;
color: #fff;
opacity: 0.85;
}
/* facultatif si supporté */
.connexion-champ {
height: 30px;
background-color: rgb(0 0 0 / 50%);
border: solid 0px rgb(155 145 60 / 100%);
border-radius: 5px;
color: rgb(255 255 255 / 100%);
}
.connexion-bouton {
height: 30px;
background-color: rgb(0 0 0 / 50%);
border-top: solid 0px rgb(155 145 60 / 100%);
border-left: solid 3px rgb(155 145 60 / 100%);
border-right: solid 3px rgb(155 145 60 / 100%);
border-bottom: solid 0px rgb(155 145 60 / 100%);
border-radius: 5px;
color: rgb(255 255 255 / 100%);
padding: 0px 10px 0px 10px;
}
.connexion-bouton:hover {
background-color: rgb(0 0 0 / 20%);
cursor: pointer;
}
[hidden] { display: none !important; }
@font-face {
font-family: 'Electrolize';
src: url('../fonts/Electrolize-Regular.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: Electrolize, Arial, sans-serif;
}
html, body {
height: 100%;
}
body {
background: #1a1a1a;
color: white;
min-height: 100vh;
}
a {
color: #ffffff;
text-decoration: none;
}
a:hover {
color: #a29b78 !important;
text-decoration: none;
}
.footer {
position: absolute;
bottom: 0px;
left: 50%;
transform: translate(-50%, -50%);
width: -50%;
height: 50px;
}
/* Utilitaires texte */
.txt-s05 { font-size: 5px; }
.txt-s10 { font-size: 10px; }
.txt-s12 { font-size: 12px; }
.txt-s15 { font-size: 15px; }
.txt-s20 { font-size: 20px; }
.txt-s22 { font-size: 22px; }
.txt-s25 { font-size: 25px; }
.txt-s40 { font-size: 40px; }
.padding5 { padding: 5px; }
.padding15 { padding: 15px; }
.padding25 { padding: 25px; }
.padding50 { padding: 50px; }
.padding-left50 { padding-left: 50px !important; }
.txt-bold { font-weight: bold; }
.txt-italic { font-style: italic; }
.txt-justify { text-align: justify; text-justify: inter-word; }
.txt-center { text-align: center !important; }
.txt-center input { display: inline-block !important; text-align: center !important; }
.txt-underline { text-decoration: underline; }
.float-right { float: right; }
.float-left { float: left; }
.txt-or { color: #a29b78; }
.txt-jaune { color: #fcba03; }
.v-hidden { visibility: hidden; }
.small-caps { font-variant-caps: small-caps; }
.ul-style {
line-height: 30px;
/* padding-left: 50px !important; */
/* border-left: 4px solid #a29b78; */
}
/* Vidéo plein écran en arrière-plan */
.video-container {
width: 100vw;
height: 100vh;
position: fixed;
inset: 0;
z-index: -5; /* derrière tout */
overflow: hidden;
}
.video-container iframe {
position: absolute;
top: 50%;
left: 50%;
width: 100vw;
height: 100vh;
transform: translate(-50%, -50%);
border: 0;
pointer-events: none; /* la vidéo nintercepte pas les clics */
}
/* Ajustements pour conserver le recadrage 16/9 sans bandes */
@media (min-aspect-ratio: 16/9) {
.video-container iframe { height: 56.25vw; }
}
@media (max-aspect-ratio: 16/9) {
.video-container iframe { width: 177.78vh; }
}
/* Voile noir par-dessus la vidéo */
.black-filter {
background: rgba(0, 0, 0, 0.7);
position: absolute;
inset: 0;
z-index: -1;
}
/* Conteneur principal (si besoin de couches au-dessus) */
.container {
width: 100%;
min-height: 100vh;
display: flex;
flex-direction: column;
position: relative;
z-index: 0; /* au-dessus de la vidéo */
}
/* Logos centrés */
.center-page-bops {
position: absolute;
top: 35%;
left: 50%;
transform: translate(-50%, -50%);
background-repeat: no-repeat;
background-position: center center;
background-size: auto;
z-index: 2;
width: 600px;
height: 600px;
}
.center-page-infos {
position: absolute;
top: 0%;
left: 50%;
transform: translate(-50%, -50%);
background-repeat: no-repeat;
background-position: center center;
background-size: auto;
z-index: 2;
width: 600px;
height: 200px;
}
.assets-div-menu {
position: fixed;
top: 25px;
left: 25px;
transform: translate(0px, 0px);
background-repeat: no-repeat;
background-position: center center;
background-size: auto;
background-color: rgb(0 0 0 / 50%);
z-index: 100;
width: 300px;
display: flex; /* Active le mode Flexbox */
flex-direction: column; /* Aligne les liens les uns sous les autres */
align-items: center; /* Centre verticalement */
gap: 2px;
justify-content: center;
border: solid 3px rgb(155 145 60 / 25%);
border-radius: 10px;
padding: 5px 0px 5px 0px;
text-align: center;
}
.connexion-div-menu {
position: fixed;
top: 25px;
right: 25px;
transform: translate(0px, 0px);
background-repeat: no-repeat;
background-position: center center;
background-size: auto;
background-color: rgb(0 0 0 / 50%);
z-index: 100;
width: 300px;
height: 35px;
display: flex; /* Active le mode Flexbox */
align-items: center; /* Centre verticalement */
justify-content: center;
border: solid 3px rgb(155 145 60 / 25%);
border-radius: 10px;
text-align: center;
}
.center-div-menu {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-repeat: no-repeat;
background-position: center center;
background-size: auto;
z-index: 3;
width: 1050px;
height: 80px;
text-align: center;
}
.center-div-menu-flex {
display: flex; /* Active l'affichage en ligne */
justify-content: center; /* Centre les items horizontalement */
align-items: center; /* Centre verticalement */
gap: 10px; /* Espace entre les divs (modifiable) */
}
.center-div-menu .menu-item {
min-height: 40px; /* hauteur de chaque div */
/* background: #ccc; /* juste pour visualiser, tu peux retirer */
/* border-radius: 5px; */
padding: 5px;
}
.center-div-menu .menu-item:hover {
min-height: 40px; /* hauteur de chaque div */
/* background: #ccc; /* juste pour visualiser, tu peux retirer */
/* border-radius: 5px; */
border-bottom: 4px solid #a29b78;
padding: 5px;
}
.center-div-menu .menu-item:hover a {
color: #a29b78 !important;
}
.menu-item-we {
min-width: 20px; /* largeur de chaque div (modifiable) */
min-height: 40px; /* hauteur de chaque div */
padding: 5px;
}
.center-page-bops {
background-image: url("../img/logo-org.png");
z-index: 2;
animation: spin-horizontal 5s linear infinite;
}
/* Animation de rotation horizontale infinie */
@keyframes spin-horizontal {
0% { transform: translate(-50%, -50%) rotateY(0deg); }
50% { transform: translate(-50%, -50%) rotateY(0deg); }
100% { transform: translate(-50%, -50%) rotateY(360deg); }
}
/* Contrôles audio en haut à droite */
.audio-controls {
position: fixed;
top: 15px;
right: 15px;
display: flex;
align-items: center;
gap: 8px;
z-index: 10;
background: rgba(0, 0, 0, 0.6);
padding: 6px 10px;
border-radius: 8px;
backdrop-filter: blur(2px);
}
.audio-controls button {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
color: white;
line-height: 1;
}
.audio-controls input[type="range"] {
width: 120px;
cursor: pointer;
accent-color: #fcba03; /* facultatif si supporté */
}
.vol-label {
font-size: 12px;
color: #fff;
opacity: 0.85;
}
/* facultatif si supporté */
.connexion-champ {
height: 30px;
background-color: rgb(0 0 0 / 50%);
border: solid 0px rgb(155 145 60 / 100%);
border-radius: 5px;
color: rgb(255 255 255 / 100%);
}
.connexion-bouton {
height: 30px;
background-color: rgb(0 0 0 / 50%);
border-top: solid 0px rgb(155 145 60 / 100%);
border-left: solid 3px rgb(155 145 60 / 100%);
border-right: solid 3px rgb(155 145 60 / 100%);
border-bottom: solid 0px rgb(155 145 60 / 100%);
border-radius: 5px;
color: rgb(255 255 255 / 100%);
padding: 0px 10px 0px 10px;
}
.connexion-bouton:hover {
background-color: rgb(0 0 0 / 20%);
cursor: pointer;
}
/* Auth / admin helpers */
.connexion-div-menu {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.connexion-div-menu #accountLabel {
display: inline-block;
}
.connexion-div-menu.is-authenticated {
cursor: default;
height: auto;
min-height: 78px;
padding: 10px 14px;
flex-direction: column;
justify-content: center;
gap: 10px;
}
.connexion-div-menu.is-authenticated #accountLabel {
font-size: 18px;
font-weight: 400;
line-height: 1.2;
color: #f7edcf;
}
.connexion-div-menu.md-trigger,
.connexion-div-menu.md-trigger #accountLabel {
cursor: pointer;
user-select: none;
}
.connexion-actions[hidden] { display: none !important; }
.connexion-actions {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.connexion-action-btn {
width: 42px;
height: 42px;
display: inline-flex;
align-items: center;
justify-content: center;
color: #f4e3b2;
text-decoration: none;
background: rgba(0, 0, 0, 0.35);
border: 1px solid rgba(244, 227, 178, 0.35);
border-radius: 10px;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease, transform 0.2s ease;
}
.connexion-action-btn:hover {
color: #ffffff;
background: rgba(155, 145, 60, 0.28);
border-color: rgba(255, 255, 255, 0.55);
transform: translateY(-1px);
}
.connexion-action-btn:focus-visible {
outline: 2px solid rgba(255, 255, 255, 0.75);
outline-offset: 2px;
}
.connexion-action-icon {
display: inline-flex;
width: 20px;
height: 20px;
}
.connexion-action-icon svg {
width: 100%;
height: 100%;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.login-status {
min-height: 18px;
}
.login-status.is-error {
color: #ff8080;
}
.login-status.is-success {
color: #9fe29f;
}
.assets-div-menu a,
.connexion-div-menu a,
.connexion-div-menu #accountLabel {
max-width: 100%;
word-break: break-word;
}
@media (max-width: 1200px) {
.center-page-bops {
width: min(48vw, 480px);
height: min(48vw, 480px);
top: 32%;
}
.center-div-menu {
width: min(1050px, calc(100vw - 40px));
}
}
@media (max-width: 900px) {
.assets-div-menu,
.connexion-div-menu {
width: auto;
max-width: none;
left: 16px;
right: 16px;
}
.assets-div-menu {
top: 16px;
}
.connexion-div-menu {
top: auto;
bottom: 16px;
justify-content: center;
min-height: 44px;
}
.center-page-bops {
width: min(62vw, 340px);
height: min(62vw, 340px);
top: 28%;
}
.center-div-menu {
width: calc(100vw - 32px);
top: 56%;
}
.center-div-menu .padding50 {
padding: 24px !important;
}
}
@media (max-width: 640px) {
.assets-div-menu {
gap: 6px;
padding: 10px 12px;
}
.connexion-div-menu {
gap: 8px;
padding: 8px 12px;
}
.connexion-div-menu.is-authenticated {
min-height: 84px;
padding: 10px 12px;
}
.connexion-actions {
flex-wrap: wrap;
justify-content: center;
}
.center-page-bops {
top: 170px;
width: min(68vw, 280px);
height: min(68vw, 280px);
}
.center-div-menu {
position: relative;
top: auto;
left: auto;
transform: none;
margin: 230px auto 100px;
width: calc(100vw - 24px);
}
.center-div-menu .padding50 {
padding: 18px !important;
}
.txt-s40 {
font-size: clamp(28px, 7vw, 40px);
}
.txt-s22 {
font-size: clamp(18px, 4.6vw, 22px);
}
}
@media (max-width: 480px) {
.assets-div-menu,
.connexion-div-menu {
left: 12px;
right: 12px;
}
.center-div-menu {
width: calc(100vw - 16px);
margin-top: 210px;
}
}

18970
database/full.sql Normal file

File diff suppressed because one or more lines are too long

402
database/schema.sql Normal file
View File

@ -0,0 +1,402 @@
/*M!999999\- enable the sandbox mode */
-- MariaDB dump 10.19 Distrib 10.11.14-MariaDB, for debian-linux-gnu (x86_64)
--
-- Host: 127.0.0.1 Database: app_39514
-- ------------------------------------------------------
-- Server version 10.11.14-MariaDB-0+deb12u2
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `tbl_auth`
--
DROP TABLE IF EXISTS `tbl_auth`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tbl_auth` (
`cl_auth_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`cl_auth_user` varchar(190) NOT NULL,
`cl_auth_pass` varchar(255) NOT NULL,
`cl_auth_right` enum('admin','moderator','member') NOT NULL DEFAULT 'member',
PRIMARY KEY (`cl_auth_id`),
UNIQUE KEY `cl_auth_user` (`cl_auth_user`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tbl_page_access`
--
DROP TABLE IF EXISTS `tbl_page_access`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tbl_page_access` (
`cl_page_access_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`cl_page_key` varchar(190) NOT NULL,
`cl_page_file` varchar(190) NOT NULL,
`cl_page_label` varchar(190) NOT NULL,
`cl_allow_admin` tinyint(1) NOT NULL DEFAULT 1,
`cl_allow_moderator` tinyint(1) NOT NULL DEFAULT 0,
`cl_allow_member` tinyint(1) NOT NULL DEFAULT 0,
`cl_updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`cl_page_access_id`),
UNIQUE KEY `cl_page_key` (`cl_page_key`),
UNIQUE KEY `cl_page_file` (`cl_page_file`)
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tbl_scbanners`
--
DROP TABLE IF EXISTS `tbl_scbanners`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tbl_scbanners` (
`cl_scbanner_id` int(11) NOT NULL AUTO_INCREMENT,
`cl_scbanner_name` varchar(255) NOT NULL,
`cl_scbanner_url` text NOT NULL,
`cl_scbanner_border_color` varchar(20) NOT NULL DEFAULT '#ffae00',
PRIMARY KEY (`cl_scbanner_id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tbl_sccharacteritems`
--
DROP TABLE IF EXISTS `tbl_sccharacteritems`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tbl_sccharacteritems` (
`cl_sccharacteritem_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`cl_sccharacteritem_character_id` int(10) unsigned NOT NULL,
`cl_sccharacteritem_source` enum('base','custom') NOT NULL DEFAULT 'base',
`cl_sccharacteritem_scobjs_id` int(10) unsigned DEFAULT NULL,
`cl_sccharacteritem_scitemcustom_id` int(11) DEFAULT NULL,
`cl_sccharacteritem_slot` varchar(120) NOT NULL DEFAULT '',
`cl_sccharacteritem_quantity` int(10) unsigned DEFAULT NULL,
`cl_sccharacteritem_note` text DEFAULT NULL,
`cl_sccharacteritem_sort_order` int(10) unsigned NOT NULL DEFAULT 0,
`cl_sccharacteritem_created_at` datetime NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`cl_sccharacteritem_id`),
KEY `idx_sccharacteritem_character` (`cl_sccharacteritem_character_id`),
KEY `idx_sccharacteritem_scobjs` (`cl_sccharacteritem_scobjs_id`),
KEY `idx_sccharacteritem_scitemcustom` (`cl_sccharacteritem_scitemcustom_id`),
KEY `idx_sccharacteritem_character_sort` (`cl_sccharacteritem_character_id`,`cl_sccharacteritem_sort_order`,`cl_sccharacteritem_id`),
CONSTRAINT `fk_sccharacteritem_character` FOREIGN KEY (`cl_sccharacteritem_character_id`) REFERENCES `tbl_sccharacters` (`cl_sccharacter_id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `fk_sccharacteritem_scitemcustom` FOREIGN KEY (`cl_sccharacteritem_scitemcustom_id`) REFERENCES `tbl_scitemcustom` (`cl_scitemcustom_id`) ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT `fk_sccharacteritem_scobjs` FOREIGN KEY (`cl_sccharacteritem_scobjs_id`) REFERENCES `tbl_scobjs` (`cl_scobjs_id`) ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tbl_sccharacters`
--
DROP TABLE IF EXISTS `tbl_sccharacters`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tbl_sccharacters` (
`cl_sccharacter_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`cl_sccharacter_owner_auth_id` int(10) unsigned NOT NULL,
`cl_sccharacter_name` varchar(190) NOT NULL,
`cl_sccharacter_role` varchar(190) NOT NULL DEFAULT '',
`cl_sccharacter_faction` varchar(190) NOT NULL DEFAULT '',
`cl_sccharacter_org_rsi_url` varchar(255) NOT NULL DEFAULT '',
`cl_sccharacter_is_player` tinyint(1) NOT NULL DEFAULT 0,
`cl_sccharacter_player_handle` varchar(190) NOT NULL DEFAULT '',
`cl_sccharacter_avatar_url` varchar(255) NOT NULL DEFAULT '',
`cl_sccharacter_description` text DEFAULT NULL,
`cl_sccharacter_notes` text DEFAULT NULL,
`cl_sccharacter_share_token` varchar(64) NOT NULL,
`cl_sccharacter_share_enabled` tinyint(1) NOT NULL DEFAULT 0,
`cl_sccharacter_is_pinned` tinyint(1) NOT NULL DEFAULT 0,
`cl_sccharacter_category_order` text DEFAULT NULL,
`cl_sccharacter_created_at` datetime NOT NULL DEFAULT current_timestamp(),
`cl_sccharacter_updated_at` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`cl_sccharacter_id`),
UNIQUE KEY `uq_sccharacter_share_token` (`cl_sccharacter_share_token`),
KEY `idx_sccharacter_owner` (`cl_sccharacter_owner_auth_id`),
KEY `idx_sccharacter_name` (`cl_sccharacter_name`),
CONSTRAINT `fk_sccharacter_owner_auth` FOREIGN KEY (`cl_sccharacter_owner_auth_id`) REFERENCES `tbl_auth` (`cl_auth_id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tbl_scitemcustom`
--
DROP TABLE IF EXISTS `tbl_scitemcustom`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tbl_scitemcustom` (
`cl_scitemcustom_id` int(11) NOT NULL AUTO_INCREMENT,
`cl_scitemcustom_owner_auth_id` int(10) unsigned DEFAULT NULL,
`cl_scitemcustom_obj_id` int(10) unsigned NOT NULL,
`cl_scitemcustom_created_at` datetime NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`cl_scitemcustom_id`),
KEY `idx_scitemcustom_obj` (`cl_scitemcustom_obj_id`),
KEY `idx_scitemcustom_owner` (`cl_scitemcustom_owner_auth_id`),
CONSTRAINT `fk_scitemcustom_obj` FOREIGN KEY (`cl_scitemcustom_obj_id`) REFERENCES `tbl_scobjs` (`cl_scobjs_id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `fk_scitemcustom_owner_auth` FOREIGN KEY (`cl_scitemcustom_owner_auth_id`) REFERENCES `tbl_auth` (`cl_auth_id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tbl_scitemcustomstat`
--
DROP TABLE IF EXISTS `tbl_scitemcustomstat`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tbl_scitemcustomstat` (
`cl_scitemcustomstat_id` int(11) NOT NULL AUTO_INCREMENT,
`cl_scitemcustomstat_itemcustom_id` int(11) NOT NULL,
`cl_scitemcustomstat_stat_id` int(11) NOT NULL,
`cl_scitemcustomstat_sign` enum('+','','-') NOT NULL DEFAULT '+',
`cl_scitemcustomstat_value` decimal(10,2) NOT NULL DEFAULT 0.00,
`cl_scitemcustomstat_created_at` datetime NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`cl_scitemcustomstat_id`),
UNIQUE KEY `uq_scitemcustomstat_item_stat` (`cl_scitemcustomstat_itemcustom_id`,`cl_scitemcustomstat_stat_id`),
KEY `idx_scitemcustomstat_item` (`cl_scitemcustomstat_itemcustom_id`),
KEY `idx_scitemcustomstat_stat` (`cl_scitemcustomstat_stat_id`),
CONSTRAINT `fk_scitemcustomstat_item` FOREIGN KEY (`cl_scitemcustomstat_itemcustom_id`) REFERENCES `tbl_scitemcustom` (`cl_scitemcustom_id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `fk_scitemcustomstat_stat` FOREIGN KEY (`cl_scitemcustomstat_stat_id`) REFERENCES `tbl_scstatsitem` (`cl_scstatsitem_id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tbl_scmanufactures`
--
DROP TABLE IF EXISTS `tbl_scmanufactures`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tbl_scmanufactures` (
`cl_scmanufactures_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`cl_scmanufactures_name` varchar(255) NOT NULL,
PRIMARY KEY (`cl_scmanufactures_id`),
UNIQUE KEY `cl_scmanufactures_name` (`cl_scmanufactures_name`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tbl_scmanutentionitems`
--
DROP TABLE IF EXISTS `tbl_scmanutentionitems`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tbl_scmanutentionitems` (
`cl_scmanutentionitem_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`cl_scmanutentionitem_manutention_id` int(10) unsigned NOT NULL,
`cl_scmanutentionitem_source` enum('base','custom') NOT NULL DEFAULT 'base',
`cl_scmanutentionitem_scobjs_id` int(10) unsigned DEFAULT NULL,
`cl_scmanutentionitem_scitemcustom_id` int(11) DEFAULT NULL,
`cl_scmanutentionitem_quantity` int(10) unsigned NOT NULL DEFAULT 1,
`cl_scmanutentionitem_extra_info` text DEFAULT NULL,
`cl_scmanutentionitem_sort_order` int(10) unsigned NOT NULL DEFAULT 0,
`cl_scmanutentionitem_created_at` datetime NOT NULL DEFAULT current_timestamp(),
`cl_scmanutentionitem_updated_at` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`cl_scmanutentionitem_id`),
KEY `idx_scmanutentionitem_sheet` (`cl_scmanutentionitem_manutention_id`),
KEY `idx_scmanutentionitem_scobjs` (`cl_scmanutentionitem_scobjs_id`),
KEY `idx_scmanutentionitem_scitemcustom` (`cl_scmanutentionitem_scitemcustom_id`),
KEY `idx_scmanutentionitem_sheet_sort` (`cl_scmanutentionitem_manutention_id`,`cl_scmanutentionitem_sort_order`,`cl_scmanutentionitem_id`),
CONSTRAINT `fk_scmanutentionitem_scitemcustom` FOREIGN KEY (`cl_scmanutentionitem_scitemcustom_id`) REFERENCES `tbl_scitemcustom` (`cl_scitemcustom_id`) ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT `fk_scmanutentionitem_scobjs` FOREIGN KEY (`cl_scmanutentionitem_scobjs_id`) REFERENCES `tbl_scobjs` (`cl_scobjs_id`) ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT `fk_scmanutentionitem_sheet` FOREIGN KEY (`cl_scmanutentionitem_manutention_id`) REFERENCES `tbl_scmanutentions` (`cl_scmanutention_id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tbl_scmanutentions`
--
DROP TABLE IF EXISTS `tbl_scmanutentions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tbl_scmanutentions` (
`cl_scmanutention_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`cl_scmanutention_owner_auth_id` int(10) unsigned NOT NULL,
`cl_scmanutention_title` varchar(190) NOT NULL,
`cl_scmanutention_type` varchar(120) NOT NULL DEFAULT '',
`cl_scmanutention_subtype` varchar(120) NOT NULL DEFAULT '',
`cl_scmanutention_description` text DEFAULT NULL,
`cl_scmanutention_share_token` varchar(64) NOT NULL,
`cl_scmanutention_share_enabled` tinyint(1) NOT NULL DEFAULT 0,
`cl_scmanutention_created_at` datetime NOT NULL DEFAULT current_timestamp(),
`cl_scmanutention_updated_at` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`cl_scmanutention_id`),
UNIQUE KEY `uq_scmanutention_share_token` (`cl_scmanutention_share_token`),
KEY `idx_scmanutention_owner` (`cl_scmanutention_owner_auth_id`),
KEY `idx_scmanutention_title` (`cl_scmanutention_title`),
CONSTRAINT `fk_scmanutention_owner_auth` FOREIGN KEY (`cl_scmanutention_owner_auth_id`) REFERENCES `tbl_auth` (`cl_auth_id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tbl_scmining`
--
DROP TABLE IF EXISTS `tbl_scmining`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tbl_scmining` (
`cl_scmining_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`cl_scmining_obj_id` int(10) unsigned NOT NULL,
`cl_scmining_scan_value` int(10) unsigned DEFAULT 0,
`cl_scmining_max_occurrence` int(10) unsigned DEFAULT 1,
`cl_scmining_can_manual` tinyint(1) DEFAULT 0,
`cl_scmining_can_land` tinyint(1) DEFAULT 0,
`cl_scmining_can_space` tinyint(1) DEFAULT 0,
PRIMARY KEY (`cl_scmining_id`),
UNIQUE KEY `cl_scmining_obj_id` (`cl_scmining_obj_id`),
CONSTRAINT `fk_scmining_obj` FOREIGN KEY (`cl_scmining_obj_id`) REFERENCES `tbl_scobjs` (`cl_scobjs_id`) ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tbl_scnotifications`
--
DROP TABLE IF EXISTS `tbl_scnotifications`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tbl_scnotifications` (
`cl_scnotification_id` int(11) NOT NULL AUTO_INCREMENT,
`cl_scnotification_webhook_id` int(11) NOT NULL,
`cl_scnotification_banner_id` int(11) DEFAULT NULL,
`cl_scnotification_title` varchar(255) NOT NULL DEFAULT '',
`cl_scnotification_message` text NOT NULL,
`cl_scnotification_payload` longtext NOT NULL,
`cl_scnotification_response` longtext DEFAULT NULL,
`cl_scnotification_success` tinyint(1) NOT NULL DEFAULT 0,
`cl_scnotification_created_by` varchar(190) NOT NULL,
`cl_scnotification_created_at` datetime NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`cl_scnotification_id`),
KEY `idx_scnotification_webhook` (`cl_scnotification_webhook_id`),
KEY `idx_scnotification_banner` (`cl_scnotification_banner_id`),
CONSTRAINT `fk_scnotification_banner` FOREIGN KEY (`cl_scnotification_banner_id`) REFERENCES `tbl_scbanners` (`cl_scbanner_id`) ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT `fk_scnotification_webhook` FOREIGN KEY (`cl_scnotification_webhook_id`) REFERENCES `tbl_scwebhooks` (`cl_scwebhook_id`) ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tbl_scobjs`
--
DROP TABLE IF EXISTS `tbl_scobjs`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tbl_scobjs` (
`cl_scobjs_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`cl_scobjs_name` varchar(255) NOT NULL,
`cl_scobjs_type` varchar(100) DEFAULT NULL,
`cl_scobjs_subtype` varchar(100) DEFAULT NULL,
`cl_scobjs_uuid` varchar(100) NOT NULL,
`cl_scobjs_rarity` varchar(10) DEFAULT '',
`cl_scobjs_about` text DEFAULT NULL,
`cl_scobjs_description` text DEFAULT NULL,
PRIMARY KEY (`cl_scobjs_id`),
UNIQUE KEY `cl_scobjs_uuid` (`cl_scobjs_uuid`)
) ENGINE=InnoDB AUTO_INCREMENT=18306 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tbl_scpreset`
--
DROP TABLE IF EXISTS `tbl_scpreset`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tbl_scpreset` (
`cl_scpreset_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`cl_scpreset_manufacture_id` int(10) unsigned DEFAULT NULL,
`cl_scpreset_vaisseau_id` int(10) unsigned DEFAULT NULL,
`cl_scpreset_name` varchar(255) NOT NULL,
`cl_scpreset_manufacturer` varchar(255) NOT NULL,
`cl_scpreset_description` text DEFAULT NULL,
`cl_scpreset_link` varchar(255) NOT NULL,
`cl_scpreset_creator` varchar(255) DEFAULT 'admin',
PRIMARY KEY (`cl_scpreset_id`),
KEY `fk_scpreset_manufacture` (`cl_scpreset_manufacture_id`),
KEY `fk_scpreset_vaisseau` (`cl_scpreset_vaisseau_id`),
CONSTRAINT `fk_scpreset_manufacture` FOREIGN KEY (`cl_scpreset_manufacture_id`) REFERENCES `tbl_scmanufactures` (`cl_scmanufactures_id`),
CONSTRAINT `fk_scpreset_vaisseau` FOREIGN KEY (`cl_scpreset_vaisseau_id`) REFERENCES `tbl_scvaisseaux` (`cl_scvaisseaux_id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tbl_scstatsitem`
--
DROP TABLE IF EXISTS `tbl_scstatsitem`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tbl_scstatsitem` (
`cl_scstatsitem_id` int(11) NOT NULL AUTO_INCREMENT,
`cl_scstatsitem_name` varchar(255) NOT NULL,
`cl_scstatsitem_unit` varchar(10) NOT NULL DEFAULT '%',
`cl_scstatsitem_created_at` datetime NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`cl_scstatsitem_id`),
UNIQUE KEY `uq_scstatsitem_name` (`cl_scstatsitem_name`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tbl_scvaisseaux`
--
DROP TABLE IF EXISTS `tbl_scvaisseaux`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tbl_scvaisseaux` (
`cl_scvaisseaux_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`cl_scvaisseaux_name` varchar(255) NOT NULL,
`cl_scvaisseaux_manufacture_id` int(10) unsigned NOT NULL,
PRIMARY KEY (`cl_scvaisseaux_id`),
KEY `fk_vaisseaux_manufacture` (`cl_scvaisseaux_manufacture_id`),
CONSTRAINT `fk_vaisseaux_manufacture` FOREIGN KEY (`cl_scvaisseaux_manufacture_id`) REFERENCES `tbl_scmanufactures` (`cl_scmanufactures_id`) ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tbl_scwebhooks`
--
DROP TABLE IF EXISTS `tbl_scwebhooks`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tbl_scwebhooks` (
`cl_scwebhook_id` int(11) NOT NULL AUTO_INCREMENT,
`cl_scwebhook_name` varchar(255) NOT NULL,
`cl_scwebhook_url` text NOT NULL,
`cl_scwebhook_image_url` text NOT NULL,
`cl_scwebhook_border_color` varchar(20) NOT NULL DEFAULT '#ffae00',
`cl_scwebhook_is_forum` tinyint(1) DEFAULT 0,
PRIMARY KEY (`cl_scwebhook_id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2026-05-07 0:10:56

510
db/auth.php Normal file
View File

@ -0,0 +1,510 @@
<?php
require_once __DIR__ . '/config.php';
function auth_config_value(array $environment_keys, array $constant_keys = []): ?string
{
foreach ($environment_keys as $environment_key) {
$value = getenv($environment_key);
if ($value !== false) {
$value = trim((string) $value);
if ($value !== '') {
return $value;
}
}
}
foreach ($constant_keys as $constant_key) {
if (defined($constant_key)) {
$value = trim((string) constant($constant_key));
if ($value !== '') {
return $value;
}
}
}
return null;
}
function auth_start_session(): void
{
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
}
}
function auth_bootstrap(): void
{
static $auth_bootstrap_done = false;
if ($auth_bootstrap_done) {
return;
}
$pdo = db();
$pdo->exec(
"CREATE TABLE IF NOT EXISTS tbl_auth (
cl_auth_id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
cl_auth_user VARCHAR(190) NOT NULL UNIQUE,
cl_auth_pass VARCHAR(255) NOT NULL,
cl_auth_right ENUM('admin', 'moderator', 'member') NOT NULL DEFAULT 'member'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
);
$pdo->exec(
"CREATE TABLE IF NOT EXISTS tbl_page_access (
cl_page_access_id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
cl_page_key VARCHAR(190) NOT NULL UNIQUE,
cl_page_file VARCHAR(190) NOT NULL UNIQUE,
cl_page_label VARCHAR(190) NOT NULL,
cl_allow_admin TINYINT(1) NOT NULL DEFAULT 1,
cl_allow_moderator TINYINT(1) NOT NULL DEFAULT 0,
cl_allow_member TINYINT(1) NOT NULL DEFAULT 0,
cl_updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
);
$stmt_auth_role_column = $pdo->query("SHOW COLUMNS FROM tbl_auth LIKE 'cl_auth_right'");
$auth_role_column = $stmt_auth_role_column->fetch();
$auth_role_type = strtolower((string) ($auth_role_column['Type'] ?? ''));
if (strpos($auth_role_type, "'moderator'") === false) {
$pdo->exec("ALTER TABLE tbl_auth MODIFY cl_auth_right ENUM('admin', 'moderator', 'member') NOT NULL DEFAULT 'member'");
}
$stmt_page_access_columns = $pdo->query('SHOW COLUMNS FROM tbl_page_access');
$page_access_columns = [];
foreach ($stmt_page_access_columns->fetchAll() as $page_access_column) {
$page_access_columns[] = (string) ($page_access_column['Field'] ?? '');
}
if (!in_array('cl_allow_moderator', $page_access_columns, true)) {
$pdo->exec('ALTER TABLE tbl_page_access ADD COLUMN cl_allow_moderator TINYINT(1) NOT NULL DEFAULT 0 AFTER cl_allow_admin');
$pdo->exec('UPDATE tbl_page_access SET cl_allow_moderator = cl_allow_member');
}
$sql_count_admin = "SELECT COUNT(*) FROM tbl_auth WHERE cl_auth_right = 'admin'";
$stmt_count_admin = $pdo->query($sql_count_admin);
$cl_auth_admin_total = (int) $stmt_count_admin->fetchColumn();
if ($cl_auth_admin_total === 0) {
[$cl_auth_user, $plain_default_password] = auth_default_admin_credentials();
if ($cl_auth_user === '' || $plain_default_password === '') {
throw new RuntimeException(
"Aucun administrateur n'existe et aucun couple DEFAULT_ADMIN_USER / DEFAULT_ADMIN_PASSWORD n'est configuré."
);
}
$cl_auth_pass = password_hash($plain_default_password, PASSWORD_DEFAULT);
$cl_auth_right = 'admin';
$stmt_insert_admin = $pdo->prepare(
'INSERT INTO tbl_auth (cl_auth_user, cl_auth_pass, cl_auth_right) VALUES (:cl_auth_user, :cl_auth_pass, :cl_auth_right)'
);
$stmt_insert_admin->execute([
'cl_auth_user' => $cl_auth_user,
'cl_auth_pass' => $cl_auth_pass,
'cl_auth_right' => $cl_auth_right,
]);
}
$auth_bootstrap_done = true;
}
function auth_default_admin_credentials(): array
{
$cl_auth_user = auth_config_value(
['DEFAULT_ADMIN_USER', 'APP_DEFAULT_ADMIN_USER'],
['DEFAULT_ADMIN_USER', 'APP_DEFAULT_ADMIN_USER']
) ?? 'admin';
$plain_default_password = auth_config_value(
['DEFAULT_ADMIN_PASSWORD', 'APP_DEFAULT_ADMIN_PASSWORD'],
['DEFAULT_ADMIN_PASSWORD', 'APP_DEFAULT_ADMIN_PASSWORD']
);
if ($plain_default_password === null) {
return ['', ''];
}
return [$cl_auth_user, $plain_default_password];
}
function auth_csrf_token(): string
{
auth_start_session();
if (empty($_SESSION['csrf_token']) || !is_string($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
function auth_validate_csrf(?string $csrf_token): bool
{
auth_start_session();
if (!isset($_SESSION['csrf_token']) || !is_string($_SESSION['csrf_token'])) {
return false;
}
if ($csrf_token === null) {
return false;
}
return hash_equals($_SESSION['csrf_token'], $csrf_token);
}
function auth_is_logged_in(): bool
{
auth_start_session();
return isset($_SESSION['user']) && isset($_SESSION['role']);
}
function auth_is_admin(): bool
{
return auth_current_role() === 'admin';
}
function auth_is_moderator(): bool
{
return auth_current_role() === 'moderator';
}
function auth_valid_roles(): array
{
return ['admin', 'moderator', 'member'];
}
function auth_role_label(string $role): string
{
static $labels = [
'admin' => 'Administrateur',
'moderator' => 'Modérateur',
'member' => 'Membre',
];
return $labels[$role] ?? ucfirst($role);
}
function auth_current_user(): string
{
auth_start_session();
return isset($_SESSION['user']) ? (string) $_SESSION['user'] : '';
}
function auth_current_role(): string
{
auth_start_session();
return isset($_SESSION['role']) ? (string) $_SESSION['role'] : '';
}
function auth_flash_set(string $flash_type, string $flash_message): void
{
auth_start_session();
$_SESSION['flash'] = [
'type' => $flash_type,
'message' => $flash_message,
];
}
function auth_flash_get(): ?array
{
auth_start_session();
if (!isset($_SESSION['flash']) || !is_array($_SESSION['flash'])) {
return null;
}
$flash = $_SESSION['flash'];
unset($_SESSION['flash']);
return $flash;
}
function auth_page_basename(string $page_file): string
{
$basename = basename(trim($page_file));
if ($basename === '' || preg_match('/^[a-zA-Z0-9._-]+$/', $basename) !== 1) {
throw new InvalidArgumentException('Nom de page invalide.');
}
return $basename;
}
function auth_page_default_member_access(string $page_file): int
{
static $member_defaults = [
'scnotification.php' => 1,
'scpreset.php' => 1,
];
$page_file = auth_page_basename($page_file);
return $member_defaults[$page_file] ?? 0;
}
function auth_page_default_moderator_access(string $page_file): int
{
return auth_page_default_member_access($page_file);
}
function auth_page_access_defaults(string $page_file, string $page_label = ''): array
{
$normalized_page_file = auth_page_basename($page_file);
$normalized_page_label = trim($page_label) !== '' ? trim($page_label) : $normalized_page_file;
return [
'cl_page_key' => pathinfo($normalized_page_file, PATHINFO_FILENAME),
'cl_page_file' => $normalized_page_file,
'cl_page_label' => $normalized_page_label,
'cl_allow_admin' => 1,
'cl_allow_moderator' => auth_page_default_moderator_access($normalized_page_file),
'cl_allow_member' => auth_page_default_member_access($normalized_page_file),
];
}
function auth_page_access_ensure(string $page_file, string $page_label = ''): array
{
auth_bootstrap();
$defaults = auth_page_access_defaults($page_file, $page_label);
$pdo = db();
$stmt = $pdo->prepare(
'SELECT cl_page_access_id, cl_page_key, cl_page_file, cl_page_label, cl_allow_admin, cl_allow_moderator, cl_allow_member
FROM tbl_page_access
WHERE cl_page_file = :cl_page_file
LIMIT 1'
);
$stmt->execute([
'cl_page_file' => $defaults['cl_page_file'],
]);
$row = $stmt->fetch();
if (!$row) {
$stmt_insert = $pdo->prepare(
'INSERT INTO tbl_page_access (cl_page_key, cl_page_file, cl_page_label, cl_allow_admin, cl_allow_moderator, cl_allow_member)
VALUES (:cl_page_key, :cl_page_file, :cl_page_label, :cl_allow_admin, :cl_allow_moderator, :cl_allow_member)'
);
$stmt_insert->execute($defaults);
$stmt->execute([
'cl_page_file' => $defaults['cl_page_file'],
]);
$row = $stmt->fetch();
} elseif ($defaults['cl_page_label'] !== '' && (string) $row['cl_page_label'] !== $defaults['cl_page_label']) {
$stmt_update_label = $pdo->prepare(
'UPDATE tbl_page_access SET cl_page_label = :cl_page_label WHERE cl_page_file = :cl_page_file'
);
$stmt_update_label->execute([
'cl_page_label' => $defaults['cl_page_label'],
'cl_page_file' => $defaults['cl_page_file'],
]);
$row['cl_page_label'] = $defaults['cl_page_label'];
}
if (!$row) {
throw new RuntimeException('Impossible d\'initialiser la configuration d\'accès de la page.');
}
$row['cl_allow_admin'] = (int) ($row['cl_allow_admin'] ?? 1);
$row['cl_allow_moderator'] = (int) ($row['cl_allow_moderator'] ?? 0);
$row['cl_allow_member'] = (int) ($row['cl_allow_member'] ?? 0);
return $row;
}
function auth_user_can_access_page(string $page_file, string $page_label = ''): bool
{
auth_start_session();
auth_bootstrap();
if (!auth_is_logged_in()) {
return false;
}
$role = auth_current_role();
if ($role === 'admin') {
return true;
}
$row = auth_page_access_ensure($page_file, $page_label);
if ($role === 'moderator') {
return (int) $row['cl_allow_moderator'] === 1;
}
if ($role === 'member') {
return (int) $row['cl_allow_member'] === 1;
}
return false;
}
function auth_require_page_access(string $page_file, string $page_label = ''): void
{
if (!auth_is_logged_in()) {
header('Location: index.php');
exit;
}
if (auth_is_admin()) {
auth_page_access_ensure($page_file, $page_label);
return;
}
if (auth_user_can_access_page($page_file, $page_label)) {
return;
}
auth_flash_set('error', 'Accès refusé : cette page n\'est pas ouverte pour votre niveau d\'autorisation.');
header('Location: index.php');
exit;
}
function auth_handle_page_access_post(string $page_file, string $page_label = ''): void
{
auth_start_session();
auth_bootstrap();
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
return;
}
if (!isset($_POST['page_access_action'])) {
return;
}
$redirect_target = auth_page_basename($page_file);
if (!auth_is_admin()) {
auth_flash_set('error', 'Seul un administrateur peut modifier les accès de page.');
header('Location: index.php');
exit;
}
$csrf_token = isset($_POST['csrf_token']) ? (string) $_POST['csrf_token'] : null;
if (!auth_validate_csrf($csrf_token)) {
auth_flash_set('error', 'Jeton CSRF invalide.');
header('Location: ' . $redirect_target);
exit;
}
$row = auth_page_access_ensure($page_file, $page_label);
$cl_allow_moderator = isset($_POST['cl_allow_moderator']) ? 1 : 0;
$cl_allow_member = isset($_POST['cl_allow_member']) ? 1 : 0;
$stmt = db()->prepare(
'UPDATE tbl_page_access
SET cl_page_label = :cl_page_label,
cl_allow_admin = 1,
cl_allow_moderator = :cl_allow_moderator,
cl_allow_member = :cl_allow_member
WHERE cl_page_file = :cl_page_file'
);
$stmt->execute([
'cl_page_label' => $row['cl_page_label'],
'cl_allow_moderator' => $cl_allow_moderator,
'cl_allow_member' => $cl_allow_member,
'cl_page_file' => $row['cl_page_file'],
]);
auth_flash_set('success', 'Accès mis à jour pour ' . $row['cl_page_label'] . '.');
header('Location: ' . $redirect_target);
exit;
}
function auth_render_page_access_widget(string $page_file, string $page_label = ''): string
{
if (!auth_is_admin()) {
return '';
}
$row = auth_page_access_ensure($page_file, $page_label);
$csrf_token = auth_csrf_token();
$action = htmlspecialchars($row['cl_page_file'], ENT_QUOTES, 'UTF-8');
$label = htmlspecialchars((string) $row['cl_page_label'], ENT_QUOTES, 'UTF-8');
$csrf = htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8');
$moderator_checked = (int) $row['cl_allow_moderator'] === 1 ? 'checked' : '';
$member_checked = (int) $row['cl_allow_member'] === 1 ? 'checked' : '';
return <<<HTML
<div style="position:fixed;top:10px;right:10px;z-index:9999;background:rgba(10,14,18,0.94);border:1px solid rgba(162,155,120,0.45);border-radius:12px;padding:10px 12px;box-shadow:0 10px 24px rgba(0,0,0,0.35);backdrop-filter:blur(8px);font-family:Arial,sans-serif;color:#f2f2f2;min-width:220px;max-width:min(92vw,280px);">
<form method="post" action="{$action}" style="margin:0;display:flex;flex-direction:column;gap:8px;">
<input type="hidden" name="csrf_token" value="{$csrf}">
<input type="hidden" name="page_access_action" value="save">
<div style="font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:#a29b78;">Accès page</div>
<div style="font-size:14px;font-weight:700;line-height:1.25;">{$label}</div>
<label style="display:flex;align-items:center;gap:8px;font-size:13px;opacity:.9;">
<input type="checkbox" checked disabled>
<span>Admin <small style="opacity:.7;">(toujours autorisé)</small></span>
</label>
<label style="display:flex;align-items:center;gap:8px;font-size:13px;">
<input type="checkbox" name="cl_allow_moderator" value="1" {$moderator_checked}>
<span>Modérateur</span>
</label>
<label style="display:flex;align-items:center;gap:8px;font-size:13px;">
<input type="checkbox" name="cl_allow_member" value="1" {$member_checked}>
<span>Membre</span>
</label>
<button type="submit" style="appearance:none;border:0;border-radius:8px;padding:8px 10px;background:#a29b78;color:#111;font-weight:700;cursor:pointer;">Appliquer</button>
</form>
</div>
HTML;
}
function auth_navigation_items(): array
{
return [
['file' => 'admin.php', 'label' => 'Utilisateurs', 'admin_only' => true],
['file' => 'scwebhook.php', 'label' => 'WEBHOOK'],
['file' => 'scnotification.php', 'label' => 'NOTIF DISCORD'],
['file' => 'scitems.php', 'label' => 'Base d\'Objets'],
['file' => 'scstatsitem.php', 'label' => 'Stats Item'],
['file' => 'scitemcustom.php', 'label' => 'Objets perso.'],
['file' => 'sccharacters.php', 'label' => 'Personnages'],
['file' => 'scmanutention.php', 'label' => 'Manutention'],
['file' => 'scmining.php', 'label' => 'Scanner Minage'],
['file' => 'scmanufactures.php', 'label' => 'Manufactures'],
['file' => 'scvaisseaux.php', 'label' => 'Vaisseaux'],
['file' => 'scpreset.php', 'label' => 'Presets Vaisseau'],
];
}
function auth_render_app_nav(string $current_page): string
{
if (!auth_is_logged_in()) {
return '';
}
$current_page = auth_page_basename($current_page);
$html = '<nav class="nav-tabs">';
foreach (auth_navigation_items() as $item) {
$file = (string) $item['file'];
$label = (string) $item['label'];
$admin_only = !empty($item['admin_only']);
if ($admin_only && !auth_is_admin()) {
continue;
}
if (!auth_is_admin() && !auth_user_can_access_page($file, $label)) {
continue;
}
$is_active = $file === $current_page ? ' class="active"' : '';
$html .= '<a href="' . htmlspecialchars($file, ENT_QUOTES, 'UTF-8') . '"' . $is_active . '>' . htmlspecialchars($label, ENT_QUOTES, 'UTF-8') . '</a>';
}
$html .= '</nav>';
return $html;
}

21
db/config.php Normal file
View File

@ -0,0 +1,21 @@
<?php
// Generated by setup_mariadb_project.sh — edit as needed.
define('DB_HOST', '127.0.0.1');
define('DB_NAME', 'app_39514');
define('DB_USER', 'app_39514');
define('DB_PASS', 'ee6da88c-09af-4b48-b728-7a55edfb4e42');
if (!defined('DISCORD_BOT_TOKEN')) {
define('DISCORD_BOT_TOKEN', 'MTQyNTgwNjIxOTMwNTY4MTAxOA.GGq1cp.CjHC7vQogGrX_HS2WnuXLf4XOLAnWLC5Cm-XA4');
}
function db() {
static $pdo;
if (!$pdo) {
$pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
}
return $pdo;
}

721
db/sccharacters.php Normal file
View File

@ -0,0 +1,721 @@
<?php
require_once __DIR__ . '/config.php';
function sccharacters_column_exists(PDO $db, string $table, string $column): bool
{
$stmt = $db->query("SHOW COLUMNS FROM `{$table}` LIKE " . $db->quote($column));
return (bool) $stmt->fetch();
}
function sccharacters_index_exists(PDO $db, string $table, string $index): bool
{
$stmt = $db->query("SHOW INDEX FROM `{$table}` WHERE Key_name = " . $db->quote($index));
return (bool) $stmt->fetch();
}
function sccharacters_foreign_key_exists(PDO $db, string $table, string $constraint): bool
{
$stmt = $db->prepare(
"SELECT COUNT(*)
FROM information_schema.TABLE_CONSTRAINTS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = :table_name
AND CONSTRAINT_NAME = :constraint_name
AND CONSTRAINT_TYPE = 'FOREIGN KEY'"
);
$stmt->execute([
'table_name' => $table,
'constraint_name' => $constraint,
]);
return (int) $stmt->fetchColumn() > 0;
}
function sccharacters_bootstrap(): void
{
static $bootstrapped = false;
if ($bootstrapped) {
return;
}
$db = db();
$db->exec(
"CREATE TABLE IF NOT EXISTS tbl_sccharacters (
cl_sccharacter_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
cl_sccharacter_owner_auth_id INT UNSIGNED NOT NULL,
cl_sccharacter_name VARCHAR(190) NOT NULL,
cl_sccharacter_role VARCHAR(190) NOT NULL DEFAULT '',
cl_sccharacter_faction VARCHAR(190) NOT NULL DEFAULT '',
cl_sccharacter_org_rsi_url VARCHAR(255) NOT NULL DEFAULT '',
cl_sccharacter_player_handle VARCHAR(190) NOT NULL DEFAULT '',
cl_sccharacter_avatar_url VARCHAR(255) NOT NULL DEFAULT '',
cl_sccharacter_description TEXT DEFAULT NULL,
cl_sccharacter_notes TEXT DEFAULT NULL,
cl_sccharacter_share_token VARCHAR(64) NOT NULL,
cl_sccharacter_share_enabled TINYINT(1) NOT NULL DEFAULT 0,
cl_sccharacter_is_pinned TINYINT(1) NOT NULL DEFAULT 0,
cl_sccharacter_category_order TEXT DEFAULT NULL,
cl_sccharacter_created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
cl_sccharacter_updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (cl_sccharacter_id),
UNIQUE KEY uq_sccharacter_share_token (cl_sccharacter_share_token),
KEY idx_sccharacter_owner (cl_sccharacter_owner_auth_id),
KEY idx_sccharacter_name (cl_sccharacter_name),
CONSTRAINT fk_sccharacter_owner_auth FOREIGN KEY (cl_sccharacter_owner_auth_id)
REFERENCES tbl_auth (cl_auth_id)
ON DELETE CASCADE
ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
);
if (!sccharacters_column_exists($db, 'tbl_sccharacters', 'cl_sccharacter_owner_auth_id')) {
$db->exec(
'ALTER TABLE tbl_sccharacters
ADD COLUMN cl_sccharacter_owner_auth_id INT UNSIGNED NULL AFTER cl_sccharacter_id'
);
}
if (!sccharacters_column_exists($db, 'tbl_sccharacters', 'cl_sccharacter_share_token')) {
$db->exec(
"ALTER TABLE tbl_sccharacters
ADD COLUMN cl_sccharacter_share_token VARCHAR(64) NOT NULL DEFAULT '' AFTER cl_sccharacter_notes"
);
}
if (!sccharacters_column_exists($db, 'tbl_sccharacters', 'cl_sccharacter_share_enabled')) {
$db->exec(
'ALTER TABLE tbl_sccharacters
ADD COLUMN cl_sccharacter_share_enabled TINYINT(1) NOT NULL DEFAULT 0 AFTER cl_sccharacter_share_token'
);
}
if (!sccharacters_column_exists($db, 'tbl_sccharacters', 'cl_sccharacter_is_pinned')) {
$db->exec(
'ALTER TABLE tbl_sccharacters
ADD COLUMN cl_sccharacter_is_pinned TINYINT(1) NOT NULL DEFAULT 0 AFTER cl_sccharacter_share_enabled'
);
}
if (!sccharacters_column_exists($db, 'tbl_sccharacters', 'cl_sccharacter_org_rsi_url')) {
$db->exec(
"ALTER TABLE tbl_sccharacters
ADD COLUMN cl_sccharacter_org_rsi_url VARCHAR(255) NOT NULL DEFAULT '' AFTER cl_sccharacter_faction"
);
}
if (!sccharacters_column_exists($db, 'tbl_sccharacters', 'cl_sccharacter_player_handle')) {
$db->exec(
"ALTER TABLE tbl_sccharacters
ADD COLUMN cl_sccharacter_player_handle VARCHAR(190) NOT NULL DEFAULT '' AFTER cl_sccharacter_org_rsi_url"
);
}
if (!sccharacters_column_exists($db, 'tbl_sccharacters', 'cl_sccharacter_category_order')) {
$db->exec(
'ALTER TABLE tbl_sccharacters
ADD COLUMN cl_sccharacter_category_order TEXT NULL AFTER cl_sccharacter_is_pinned'
);
}
if (!sccharacters_index_exists($db, 'tbl_sccharacters', 'idx_sccharacter_owner')) {
$db->exec(
'ALTER TABLE tbl_sccharacters
ADD INDEX idx_sccharacter_owner (cl_sccharacter_owner_auth_id)'
);
}
if (!sccharacters_index_exists($db, 'tbl_sccharacters', 'uq_sccharacter_share_token')) {
$db->exec(
'ALTER TABLE tbl_sccharacters
ADD UNIQUE KEY uq_sccharacter_share_token (cl_sccharacter_share_token)'
);
}
if (!sccharacters_foreign_key_exists($db, 'tbl_sccharacters', 'fk_sccharacter_owner_auth')) {
$db->exec(
'ALTER TABLE tbl_sccharacters
ADD CONSTRAINT fk_sccharacter_owner_auth FOREIGN KEY (cl_sccharacter_owner_auth_id)
REFERENCES tbl_auth (cl_auth_id)
ON DELETE CASCADE
ON UPDATE CASCADE'
);
}
$db->exec(
"CREATE TABLE IF NOT EXISTS tbl_sccharacteritems (
cl_sccharacteritem_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
cl_sccharacteritem_character_id INT UNSIGNED NOT NULL,
cl_sccharacteritem_source ENUM('base', 'custom') NOT NULL DEFAULT 'base',
cl_sccharacteritem_scobjs_id INT UNSIGNED DEFAULT NULL,
cl_sccharacteritem_scitemcustom_id INT(11) DEFAULT NULL,
cl_sccharacteritem_slot VARCHAR(120) NOT NULL DEFAULT '',
cl_sccharacteritem_quantity INT UNSIGNED DEFAULT NULL,
cl_sccharacteritem_note TEXT DEFAULT NULL,
cl_sccharacteritem_sort_order INT UNSIGNED NOT NULL DEFAULT 0,
cl_sccharacteritem_created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (cl_sccharacteritem_id),
KEY idx_sccharacteritem_character (cl_sccharacteritem_character_id),
KEY idx_sccharacteritem_scobjs (cl_sccharacteritem_scobjs_id),
KEY idx_sccharacteritem_scitemcustom (cl_sccharacteritem_scitemcustom_id),
KEY idx_sccharacteritem_character_sort (cl_sccharacteritem_character_id, cl_sccharacteritem_sort_order, cl_sccharacteritem_id),
CONSTRAINT fk_sccharacteritem_character FOREIGN KEY (cl_sccharacteritem_character_id)
REFERENCES tbl_sccharacters (cl_sccharacter_id)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT fk_sccharacteritem_scobjs FOREIGN KEY (cl_sccharacteritem_scobjs_id)
REFERENCES tbl_scobjs (cl_scobjs_id)
ON DELETE SET NULL
ON UPDATE CASCADE,
CONSTRAINT fk_sccharacteritem_scitemcustom FOREIGN KEY (cl_sccharacteritem_scitemcustom_id)
REFERENCES tbl_scitemcustom (cl_scitemcustom_id)
ON DELETE SET NULL
ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
);
if (!sccharacters_column_exists($db, 'tbl_sccharacteritems', 'cl_sccharacteritem_scobjs_id')) {
$db->exec(
'ALTER TABLE tbl_sccharacteritems
ADD COLUMN cl_sccharacteritem_scobjs_id INT UNSIGNED NULL AFTER cl_sccharacteritem_source'
);
}
if (!sccharacters_column_exists($db, 'tbl_sccharacteritems', 'cl_sccharacteritem_scitemcustom_id')) {
$db->exec(
'ALTER TABLE tbl_sccharacteritems
ADD COLUMN cl_sccharacteritem_scitemcustom_id INT(11) NULL AFTER cl_sccharacteritem_scobjs_id'
);
}
if (!sccharacters_column_exists($db, 'tbl_sccharacteritems', 'cl_sccharacteritem_quantity')) {
$db->exec(
'ALTER TABLE tbl_sccharacteritems
ADD COLUMN cl_sccharacteritem_quantity INT UNSIGNED NULL AFTER cl_sccharacteritem_slot'
);
}
if (!sccharacters_column_exists($db, 'tbl_sccharacteritems', 'cl_sccharacteritem_sort_order')) {
$db->exec(
'ALTER TABLE tbl_sccharacteritems
ADD COLUMN cl_sccharacteritem_sort_order INT UNSIGNED NOT NULL DEFAULT 0 AFTER cl_sccharacteritem_note'
);
}
if (!sccharacters_index_exists($db, 'tbl_sccharacteritems', 'idx_sccharacteritem_character')) {
$db->exec(
'ALTER TABLE tbl_sccharacteritems
ADD INDEX idx_sccharacteritem_character (cl_sccharacteritem_character_id)'
);
}
if (!sccharacters_index_exists($db, 'tbl_sccharacteritems', 'idx_sccharacteritem_scobjs')) {
$db->exec(
'ALTER TABLE tbl_sccharacteritems
ADD INDEX idx_sccharacteritem_scobjs (cl_sccharacteritem_scobjs_id)'
);
}
if (!sccharacters_index_exists($db, 'tbl_sccharacteritems', 'idx_sccharacteritem_scitemcustom')) {
$db->exec(
'ALTER TABLE tbl_sccharacteritems
ADD INDEX idx_sccharacteritem_scitemcustom (cl_sccharacteritem_scitemcustom_id)'
);
}
if (!sccharacters_index_exists($db, 'tbl_sccharacteritems', 'idx_sccharacteritem_character_sort')) {
$db->exec(
'ALTER TABLE tbl_sccharacteritems
ADD INDEX idx_sccharacteritem_character_sort (cl_sccharacteritem_character_id, cl_sccharacteritem_sort_order, cl_sccharacteritem_id)'
);
}
if (!sccharacters_foreign_key_exists($db, 'tbl_sccharacteritems', 'fk_sccharacteritem_character')) {
$db->exec(
'ALTER TABLE tbl_sccharacteritems
ADD CONSTRAINT fk_sccharacteritem_character FOREIGN KEY (cl_sccharacteritem_character_id)
REFERENCES tbl_sccharacters (cl_sccharacter_id)
ON DELETE CASCADE
ON UPDATE CASCADE'
);
}
if (!sccharacters_foreign_key_exists($db, 'tbl_sccharacteritems', 'fk_sccharacteritem_scobjs')) {
$db->exec(
'ALTER TABLE tbl_sccharacteritems
ADD CONSTRAINT fk_sccharacteritem_scobjs FOREIGN KEY (cl_sccharacteritem_scobjs_id)
REFERENCES tbl_scobjs (cl_scobjs_id)
ON DELETE SET NULL
ON UPDATE CASCADE'
);
}
if (!sccharacters_foreign_key_exists($db, 'tbl_sccharacteritems', 'fk_sccharacteritem_scitemcustom')) {
$db->exec(
'ALTER TABLE tbl_sccharacteritems
ADD CONSTRAINT fk_sccharacteritem_scitemcustom FOREIGN KEY (cl_sccharacteritem_scitemcustom_id)
REFERENCES tbl_scitemcustom (cl_scitemcustom_id)
ON DELETE SET NULL
ON UPDATE CASCADE'
);
}
$stmt_missing_tokens = $db->query(
"SELECT cl_sccharacter_id
FROM tbl_sccharacters
WHERE cl_sccharacter_share_token IS NULL
OR cl_sccharacter_share_token = ''"
);
$stmt_update_token = $db->prepare(
'UPDATE tbl_sccharacters
SET cl_sccharacter_share_token = :token
WHERE cl_sccharacter_id = :id'
);
foreach ($stmt_missing_tokens->fetchAll() as $row) {
$stmt_update_token->execute([
'token' => sccharacters_generate_share_token($db),
'id' => (int) $row['cl_sccharacter_id'],
]);
}
$bootstrapped = true;
}
function sccharacters_generate_share_token(PDO $db): string
{
do {
$token = bin2hex(random_bytes(16));
$stmt = $db->prepare(
'SELECT cl_sccharacter_id
FROM tbl_sccharacters
WHERE cl_sccharacter_share_token = :token
LIMIT 1'
);
$stmt->execute(['token' => $token]);
} while ($stmt->fetch());
return $token;
}
function sccharacters_extract_org_tag(?string $url): string
{
$url = trim((string) $url);
if ($url === '') {
return '';
}
$path = trim((string) parse_url($url, PHP_URL_PATH));
if ($path === '') {
return '';
}
if (preg_match('~/(?:[a-z]{2}/)?orgs/([^/?#]+)~i', $path, $matches)) {
$tag = rawurldecode((string) ($matches[1] ?? ''));
} else {
$segments = array_values(array_filter(explode('/', trim($path, '/')), static fn (string $segment): bool => $segment !== ''));
$tag = $segments !== [] ? rawurldecode((string) end($segments)) : '';
}
$tag = preg_replace('/[^A-Za-z0-9._-]+/', '', (string) $tag);
if (!is_string($tag)) {
return '';
}
return strtoupper(trim($tag));
}
function sccharacters_resolve_org_tag(array $character): string
{
return sccharacters_extract_org_tag((string) ($character['cl_sccharacter_org_rsi_url'] ?? ''));
}
function sccharacters_has_player_handle(array $character): bool
{
return trim((string) ($character['cl_sccharacter_player_handle'] ?? '')) !== '';
}
function sccharacters_reindex_character_items(PDO $db, int $character_id): void
{
if ($character_id <= 0) {
return;
}
$stmt = $db->prepare(
'SELECT cl_sccharacteritem_id
FROM tbl_sccharacteritems
WHERE cl_sccharacteritem_character_id = :character_id
ORDER BY
CASE WHEN cl_sccharacteritem_sort_order <= 0 THEN 0 ELSE 1 END,
cl_sccharacteritem_sort_order ASC,
cl_sccharacteritem_id ASC'
);
$stmt->execute(['character_id' => $character_id]);
$item_ids = array_map('intval', $stmt->fetchAll(PDO::FETCH_COLUMN));
if ($item_ids === []) {
return;
}
$stmt_update = $db->prepare(
'UPDATE tbl_sccharacteritems
SET cl_sccharacteritem_sort_order = :sort_order
WHERE cl_sccharacteritem_character_id = :character_id
AND cl_sccharacteritem_id = :item_id'
);
$db->beginTransaction();
try {
$position = 1;
foreach ($item_ids as $item_id) {
$stmt_update->execute([
'sort_order' => $position,
'character_id' => $character_id,
'item_id' => $item_id,
]);
$position++;
}
$db->commit();
} catch (Throwable $exception) {
if ($db->inTransaction()) {
$db->rollBack();
}
throw $exception;
}
}
function sccharacters_next_item_sort_order(PDO $db, int $character_id): int
{
if ($character_id <= 0) {
return 1;
}
sccharacters_reindex_character_items($db, $character_id);
$stmt = $db->prepare(
'SELECT COALESCE(MAX(cl_sccharacteritem_sort_order), 0)
FROM tbl_sccharacteritems
WHERE cl_sccharacteritem_character_id = :character_id'
);
$stmt->execute(['character_id' => $character_id]);
return ((int) $stmt->fetchColumn()) + 1;
}
function sccharacters_item_category_options(): array
{
return [
'weapon' => 'Armes',
'armor' => 'Armures',
'tools' => 'Outils',
'consumables' => 'Consommables',
'ammunition' => 'Munitions',
'attachments' => 'Accessoires',
'clothing' => 'Vêtements',
'cargo' => 'Cargo / Conteneurs',
'ship' => 'Composants / Véhicule',
'access' => 'Accès / Mobilier',
'misc' => 'Divers',
];
}
function sccharacters_default_category_order(): array
{
return array_keys(sccharacters_item_category_options());
}
function sccharacters_normalize_category_order(array $category_order): array
{
$known_categories = sccharacters_default_category_order();
$known_lookup = array_fill_keys($known_categories, true);
$normalized = [];
foreach ($category_order as $category_key) {
$category_key = trim((string) $category_key);
if ($category_key === '' || !isset($known_lookup[$category_key]) || isset($normalized[$category_key])) {
continue;
}
$normalized[$category_key] = $category_key;
}
foreach ($known_categories as $category_key) {
if (!isset($normalized[$category_key])) {
$normalized[$category_key] = $category_key;
}
}
return array_values($normalized);
}
function sccharacters_parse_category_order(?string $raw_value): array
{
$raw_value = trim((string) $raw_value);
if ($raw_value === '') {
return sccharacters_default_category_order();
}
$decoded = json_decode($raw_value, true);
if (!is_array($decoded)) {
return sccharacters_default_category_order();
}
return sccharacters_normalize_category_order($decoded);
}
function sccharacters_encode_category_order(array $category_order): string
{
$encoded = json_encode(
sccharacters_normalize_category_order($category_order),
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
);
return $encoded !== false
? $encoded
: json_encode(sccharacters_default_category_order(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
function sccharacters_character_category_order(array $character_row): array
{
return sccharacters_parse_category_order((string) ($character_row['cl_sccharacter_category_order'] ?? ''));
}
function sccharacters_sort_items_by_category_order(array $items_by_category, array $category_order): array
{
if ($items_by_category === []) {
return [];
}
$sorted = [];
foreach (sccharacters_normalize_category_order($category_order) as $category_key) {
if (isset($items_by_category[$category_key])) {
$sorted[$category_key] = $items_by_category[$category_key];
}
}
foreach ($items_by_category as $category_key => $category_items) {
if (!isset($sorted[$category_key])) {
$sorted[$category_key] = $category_items;
}
}
return $sorted;
}
function sccharacters_save_character_category_order(PDO $db, int $character_id, array $category_order): void
{
if ($character_id <= 0) {
return;
}
$stmt = $db->prepare(
'UPDATE tbl_sccharacters
SET cl_sccharacter_category_order = :category_order
WHERE cl_sccharacter_id = :character_id'
);
$stmt->execute([
'category_order' => sccharacters_encode_category_order($category_order),
'character_id' => $character_id,
]);
}
function sccharacters_item_category_label(string $category): string
{
$options = sccharacters_item_category_options();
return $options[$category] ?? $options['misc'];
}
function sccharacters_item_category_slug(string $value): string
{
$value = trim($value);
if ($value == '') {
return '';
}
if (function_exists('iconv')) {
$converted = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $value);
if ($converted !== false) {
$value = $converted;
}
}
$value = strtolower($value);
$value = preg_replace('/[^a-z0-9]+/', '_', $value) ?? '';
return trim($value, '_');
}
function sccharacters_string_contains_any(string $haystack, array $needles): bool
{
foreach ($needles as $needle) {
if ($needle !== '' && strpos($haystack, $needle) !== false) {
return true;
}
}
return false;
}
function sccharacters_normalize_item_category(?string $value): string
{
$slug = sccharacters_item_category_slug((string) $value);
if ($slug === '' || in_array($slug, ['auto', 'automatic', 'automatique', 'default', 'defaut'], true)) {
return '';
}
$options = sccharacters_item_category_options();
if (isset($options[$slug])) {
return $slug;
}
$exact_aliases = [
'arme' => 'weapon',
'armes' => 'weapon',
'weapon' => 'weapon',
'weapons' => 'weapon',
'armure' => 'armor',
'armures' => 'armor',
'armor' => 'armor',
'armors' => 'armor',
'outil' => 'tools',
'outils' => 'tools',
'tool' => 'tools',
'tools' => 'tools',
'consommable' => 'consumables',
'consommables' => 'consumables',
'usable' => 'consumables',
'food' => 'consumables',
'drink' => 'consumables',
'munition' => 'ammunition',
'munitions' => 'ammunition',
'ammo' => 'ammunition',
'ammunition' => 'ammunition',
'accessoire' => 'attachments',
'accessoires' => 'attachments',
'attachment' => 'attachments',
'attachments' => 'attachments',
'vetement' => 'clothing',
'vetements' => 'clothing',
'clothing' => 'clothing',
'clothes' => 'clothing',
'cargo' => 'cargo',
'container' => 'cargo',
'conteneur' => 'cargo',
'conteneurs' => 'cargo',
'composant' => 'ship',
'composants' => 'ship',
'component' => 'ship',
'components' => 'ship',
'vaisseau' => 'ship',
'vaisseaux' => 'ship',
'vehicule' => 'ship',
'vehicules' => 'ship',
'acces' => 'access',
'access' => 'access',
'mobilier' => 'access',
'divers' => 'misc',
'misc' => 'misc',
'autre' => 'misc',
'autres' => 'misc',
'other' => 'misc',
];
if (isset($exact_aliases[$slug])) {
return $exact_aliases[$slug];
}
$contains_aliases = [
'weapon' => ['arme', 'weapon', 'pistol', 'rifle', 'shotgun', 'sniper', 'knife', 'blade'],
'armor' => ['armure', 'armor', 'plating'],
'tools' => ['outil', 'tool', 'tractor', 'mining', 'multitool', 'multi_tool', 'gadget'],
'consumables' => ['consommable', 'consumable', 'usable', 'food', 'drink', 'med', 'medical', 'heal'],
'ammunition' => ['munition', 'ammo', 'ammunition', 'magazine', 'grenade', 'rocket'],
'attachments' => ['attachment', 'accessoire', 'scope', 'optic', 'silencer', 'sight', 'barrel'],
'clothing' => ['clothing', 'vetement', 'apparel', 'outfit', 'char_clothing', 'char_head'],
'cargo' => ['cargo', 'container', 'crate', 'box'],
'ship' => ['component', 'composant', 'vaisseau', 'vehicule', 'thruster', 'quantum', 'powerplant', 'cooler', 'radar', 'sensor', 'shield', 'turret', 'missilelauncher', 'docking', 'flightcontroller', 'fueltank', 'fuelintake', 'aimodule'],
'access' => ['door', 'seat', 'access', 'display', 'controlpanel', 'dashboard', 'mobilier'],
];
foreach ($contains_aliases as $category => $needles) {
if (sccharacters_string_contains_any($slug, $needles)) {
return $category;
}
}
return '';
}
function sccharacters_guess_item_category(?string $type, ?string $subtype = null): string
{
$haystack = strtolower(trim(((string) $type) . ' ' . ((string) $subtype)));
$slug = sccharacters_item_category_slug($haystack);
if ($slug === '') {
return 'misc';
}
if (sccharacters_string_contains_any($slug, ['char_clothing', 'char_head', 'clothing', 'apparel', 'outfit', 'hat', 'beard', 'piercing'])) {
return 'clothing';
}
if (sccharacters_string_contains_any($slug, ['weaponattachment', 'attachment', 'scope', 'optic', 'sight', 'silencer', 'barrel', 'mag_mount'])) {
return 'attachments';
}
if (sccharacters_string_contains_any($slug, ['missile', 'ammo', 'ammunition', 'magazine', 'rocket'])) {
return 'ammunition';
}
if (sccharacters_string_contains_any($slug, ['armor', 'armure'])) {
return 'armor';
}
if (sccharacters_string_contains_any($slug, ['weapon', 'gun', 'rifle', 'pistol', 'shotgun', 'sniper', 'knife', 'blade'])) {
return 'weapon';
}
if (sccharacters_string_contains_any($slug, ['usable', 'consumable', 'food', 'drink', 'fps_consumable', 'med', 'medical'])) {
return 'consumables';
}
if (sccharacters_string_contains_any($slug, ['tool', 'outil', 'mining', 'tractor', 'gadget', 'utility'])) {
return 'tools';
}
if (sccharacters_string_contains_any($slug, ['cargo', 'container', 'crate', 'box'])) {
return 'cargo';
}
if (sccharacters_string_contains_any($slug, ['door', 'seat', 'access', 'display', 'controlpanel', 'dashboard', 'shopdisplay', 'player'])) {
return 'access';
}
if (sccharacters_string_contains_any($slug, ['thruster', 'quantum', 'powerplant', 'cooler', 'radar', 'sensor', 'shield', 'turret', 'flightcontroller', 'fueltank', 'fuelintake', 'a_module', 'aimodule', 'docking', 'attachedpart'])) {
return 'ship';
}
return 'misc';
}
function sccharacters_resolve_item_category(?string $storedValue, ?string $type, ?string $subtype = null): string
{
$normalized = sccharacters_normalize_item_category($storedValue);
if ($normalized !== '') {
return $normalized;
}
return sccharacters_guess_item_category($type, $subtype);
}

478
db/scdiscord.php Normal file
View File

@ -0,0 +1,478 @@
<?php
require_once __DIR__ . '/config.php';
function scdiscord_bootstrap(): void
{
static $bootstrapped = false;
if ($bootstrapped) {
return;
}
$db = db();
$db->exec(
"CREATE TABLE IF NOT EXISTS tbl_scwebhooks (
cl_scwebhook_id INT(11) NOT NULL AUTO_INCREMENT,
cl_scwebhook_name VARCHAR(255) NOT NULL,
cl_scwebhook_url TEXT NOT NULL,
cl_scwebhook_image_url TEXT NOT NULL,
cl_scwebhook_border_color VARCHAR(20) NOT NULL DEFAULT '#ffae00',
cl_scwebhook_is_forum TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (cl_scwebhook_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci"
);
$columns_stmt = $db->query("SHOW COLUMNS FROM tbl_scwebhooks LIKE 'cl_scwebhook_image_url'");
$has_webhook_image = (bool) $columns_stmt->fetch();
if (!$has_webhook_image) {
$db->exec("ALTER TABLE tbl_scwebhooks ADD COLUMN cl_scwebhook_image_url TEXT NOT NULL AFTER cl_scwebhook_url");
}
$columns_stmt = $db->query("SHOW COLUMNS FROM tbl_scwebhooks LIKE 'cl_scwebhook_border_color'");
$has_webhook_border_color = (bool) $columns_stmt->fetch();
if (!$has_webhook_border_color) {
$db->exec("ALTER TABLE tbl_scwebhooks ADD COLUMN cl_scwebhook_border_color VARCHAR(20) NOT NULL DEFAULT '#ffae00' AFTER cl_scwebhook_image_url");
}
$db->exec(
"CREATE TABLE IF NOT EXISTS tbl_scbanners (
cl_scbanner_id INT(11) NOT NULL AUTO_INCREMENT,
cl_scbanner_name VARCHAR(255) NOT NULL,
cl_scbanner_url TEXT NOT NULL,
cl_scbanner_border_color VARCHAR(20) NOT NULL DEFAULT '#ffae00',
PRIMARY KEY (cl_scbanner_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci"
);
$columns_stmt = $db->query("SHOW COLUMNS FROM tbl_scbanners LIKE 'cl_scbanner_border_color'");
$has_border_color = (bool) $columns_stmt->fetch();
if (!$has_border_color) {
$db->exec("ALTER TABLE tbl_scbanners ADD COLUMN cl_scbanner_border_color VARCHAR(20) NOT NULL DEFAULT '#ffae00' AFTER cl_scbanner_url");
}
$db->exec(
"UPDATE tbl_scwebhooks w
LEFT JOIN tbl_scbanners b ON b.cl_scbanner_name = w.cl_scwebhook_name
SET
w.cl_scwebhook_image_url = CASE
WHEN (w.cl_scwebhook_image_url IS NULL OR w.cl_scwebhook_image_url = '') AND b.cl_scbanner_url IS NOT NULL THEN b.cl_scbanner_url
ELSE w.cl_scwebhook_image_url
END,
w.cl_scwebhook_border_color = CASE
WHEN (w.cl_scwebhook_border_color IS NULL OR w.cl_scwebhook_border_color = '' OR w.cl_scwebhook_border_color = '#ffae00') AND b.cl_scbanner_border_color IS NOT NULL THEN b.cl_scbanner_border_color
ELSE w.cl_scwebhook_border_color
END"
);
$db->exec(
"CREATE TABLE IF NOT EXISTS tbl_scnotifications (
cl_scnotification_id INT(11) NOT NULL AUTO_INCREMENT,
cl_scnotification_webhook_id INT(11) NOT NULL,
cl_scnotification_banner_id INT(11) DEFAULT NULL,
cl_scnotification_title VARCHAR(255) NOT NULL DEFAULT '',
cl_scnotification_message TEXT NOT NULL,
cl_scnotification_payload LONGTEXT NOT NULL,
cl_scnotification_response LONGTEXT DEFAULT NULL,
cl_scnotification_success TINYINT(1) NOT NULL DEFAULT 0,
cl_scnotification_created_by VARCHAR(190) NOT NULL,
cl_scnotification_created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (cl_scnotification_id),
KEY idx_scnotification_webhook (cl_scnotification_webhook_id),
KEY idx_scnotification_banner (cl_scnotification_banner_id),
CONSTRAINT fk_scnotification_webhook FOREIGN KEY (cl_scnotification_webhook_id)
REFERENCES tbl_scwebhooks (cl_scwebhook_id)
ON UPDATE CASCADE
ON DELETE RESTRICT,
CONSTRAINT fk_scnotification_banner FOREIGN KEY (cl_scnotification_banner_id)
REFERENCES tbl_scbanners (cl_scbanner_id)
ON UPDATE CASCADE
ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci"
);
$stmt_existing_red = $db->prepare('SELECT cl_scbanner_id FROM tbl_scbanners WHERE cl_scbanner_name = :name LIMIT 1');
$stmt_existing_red->execute(['name' => 'Alerte Rouge']);
$existing_red_banner = $stmt_existing_red->fetch(PDO::FETCH_ASSOC);
if ($existing_red_banner) {
$stmt_update_red = $db->prepare(
'UPDATE tbl_scbanners
SET cl_scbanner_border_color = CASE
WHEN cl_scbanner_border_color IS NULL OR cl_scbanner_border_color = "" OR cl_scbanner_border_color = "#ffae00"
THEN :border_color
ELSE cl_scbanner_border_color
END
WHERE cl_scbanner_id = :id'
);
$stmt_update_red->execute([
'border_color' => '#ff3b30',
'id' => $existing_red_banner['cl_scbanner_id'],
]);
}
$bootstrapped = true;
}
function scdiscord_mask_webhook_url(string $url): string
{
$trimmed = trim($url);
if ($trimmed === '') {
return '';
}
$length = strlen($trimmed);
if ($length <= 24) {
return str_repeat('•', max(8, $length));
}
return substr($trimmed, 0, 32) . str_repeat('•', 18) . substr($trimmed, -10);
}
function scdiscord_normalize_hex_color(string $color): string
{
$candidate = strtoupper(trim($color));
if ($candidate === '') {
return '#FFAE00';
}
if ($candidate[0] !== '#') {
$candidate = '#' . $candidate;
}
if (!preg_match('/^#[0-9A-F]{6}$/', $candidate)) {
return '#FFAE00';
}
return $candidate;
}
function scdiscord_hex_to_decimal(string $color): int
{
return hexdec(ltrim(scdiscord_normalize_hex_color($color), '#'));
}
function scdiscord_build_mentions(bool $notify_here, bool $notify_everyone): array
{
$parts = [];
if ($notify_here) {
$parts[] = '@here';
}
if ($notify_everyone) {
$parts[] = '@everyone';
}
return $parts;
}
function scdiscord_build_thread_name(string $title, string $location, string $start_date): string
{
$parts = [];
if ($title !== '') {
$parts[] = $title;
}
if ($location !== '') {
$parts[] = $location;
}
if ($start_date !== '') {
$parts[] = $start_date;
}
$thread_name = trim(implode(' • ', $parts));
if ($thread_name === '') {
$thread_name = 'Notification Discord';
}
return mb_substr($thread_name, 0, 100);
}
function scdiscord_post_webhook(string $webhook_url, array $payload): array
{
$target_url = $webhook_url;
$separator = (str_contains($target_url, '?')) ? '&' : '?';
$target_url .= $separator . 'wait=true';
$json_payload = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($json_payload === false) {
return [
'success' => false,
'http_code' => 0,
'response' => 'Erreur d\'encodage JSON.',
];
}
if (function_exists('curl_init')) {
$ch = curl_init($target_url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $json_payload,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Content-Length: ' . strlen($json_payload),
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 20,
]);
$response = curl_exec($ch);
$http_code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curl_error = curl_error($ch);
curl_close($ch);
if ($response === false) {
return [
'success' => false,
'http_code' => $http_code,
'response' => $curl_error !== '' ? $curl_error : 'Erreur CURL inconnue.',
];
}
return [
'success' => $http_code >= 200 && $http_code < 300,
'http_code' => $http_code,
'response' => $response,
];
}
$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => "Content-Type: application/json\r\n",
'content' => $json_payload,
'timeout' => 20,
'ignore_errors' => true,
],
]);
$response = @file_get_contents($target_url, false, $context);
$http_code = 0;
if (isset($http_response_header) && is_array($http_response_header)) {
foreach ($http_response_header as $header_line) {
if (preg_match('#^HTTP/\S+\s+(\d{3})#', $header_line, $matches)) {
$http_code = (int) $matches[1];
break;
}
}
}
return [
'success' => $response !== false && $http_code >= 200 && $http_code < 300,
'http_code' => $http_code,
'response' => $response === false ? 'Erreur lors de la requête HTTP.' : $response,
];
}
function scdiscord_get_bot_token(): string
{
$candidates = [];
if (defined('DISCORD_BOT_TOKEN') && is_string(DISCORD_BOT_TOKEN)) {
$candidates[] = DISCORD_BOT_TOKEN;
}
foreach (['DISCORD_BOT_TOKEN', 'SC_DISCORD_BOT_TOKEN', 'BOT_TOKEN'] as $env_key) {
$value = getenv($env_key);
if ($value !== false) {
$candidates[] = $value;
}
}
foreach ($candidates as $candidate) {
$token = trim((string) $candidate);
if ($token !== '') {
return $token;
}
}
return '';
}
function scdiscord_decode_json_response(?string $response): array
{
if (!is_string($response) || trim($response) === '') {
return [];
}
$decoded = json_decode($response, true);
return is_array($decoded) ? $decoded : [];
}
function scdiscord_bot_request(string $method, string $url, string $bot_token, ?array $payload = null): array
{
$headers = [
'Authorization: Bot ' . $bot_token,
];
$json_payload = null;
if ($payload !== null) {
$json_payload = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($json_payload === false) {
return [
'success' => false,
'http_code' => 0,
'response' => 'Erreur d\'encodage JSON.',
];
}
$headers[] = 'Content-Type: application/json';
$headers[] = 'Content-Length: ' . strlen($json_payload);
}
if (function_exists('curl_init')) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => strtoupper($method),
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 20,
]);
if ($json_payload !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $json_payload);
}
$response = curl_exec($ch);
$http_code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curl_error = curl_error($ch);
curl_close($ch);
if ($response === false) {
return [
'success' => false,
'http_code' => $http_code,
'response' => $curl_error !== '' ? $curl_error : 'Erreur CURL inconnue.',
];
}
return [
'success' => $http_code >= 200 && $http_code < 300,
'http_code' => $http_code,
'response' => $response,
];
}
$header_lines = implode("\r\n", $headers) . "\r\n";
$context = stream_context_create([
'http' => [
'method' => strtoupper($method),
'header' => $header_lines,
'content' => $json_payload ?? '',
'timeout' => 20,
'ignore_errors' => true,
],
]);
$response = @file_get_contents($url, false, $context);
$http_code = 0;
if (isset($http_response_header) && is_array($http_response_header)) {
foreach ($http_response_header as $header_line) {
if (preg_match('#^HTTP/\S+\s+(\d{3})#', $header_line, $matches)) {
$http_code = (int) $matches[1];
break;
}
}
}
return [
'success' => $response !== false && $http_code >= 200 && $http_code < 300,
'http_code' => $http_code,
'response' => $response === false ? 'Erreur lors de la requête HTTP.' : $response,
];
}
function scdiscord_apply_bot_actions(array $message_data, bool $use_reactions, bool $use_publicthread, string $thread_name): array
{
if (!$use_reactions && !$use_publicthread) {
return [
'success' => true,
'http_code' => 200,
'response' => 'Aucune action bot demandée.',
'details' => [],
];
}
$message_id = trim((string) ($message_data['id'] ?? ''));
$channel_id = trim((string) ($message_data['channel_id'] ?? ''));
if ($message_id === '' || $channel_id === '') {
return [
'success' => false,
'http_code' => 0,
'response' => 'Réponse Discord invalide : id de message ou channel_id manquant.',
'details' => [],
];
}
$bot_token = scdiscord_get_bot_token();
if ($bot_token === '') {
return [
'success' => false,
'http_code' => 0,
'response' => 'Token bot Discord manquant. Définis DISCORD_BOT_TOKEN côté serveur.',
'details' => [],
];
}
$details = [];
$last_http_code = 200;
$failed = false;
if ($use_reactions) {
foreach (['👍', '⌛', '❔', '👎'] as $emoji) {
$url = 'https://discord.com/api/v10/channels/' . rawurlencode($channel_id) . '/messages/' . rawurlencode($message_id) . '/reactions/' . rawurlencode($emoji) . '/@me';
$result = scdiscord_bot_request('PUT', $url, $bot_token);
$action_success = !empty($result['success']);
$last_http_code = (int) ($result['http_code'] ?? $last_http_code);
$details[] = [
'action' => 'reaction',
'emoji' => $emoji,
'success' => $action_success,
'http_code' => $last_http_code,
'response' => (string) ($result['response'] ?? ''),
];
if (!$action_success) {
$failed = true;
}
sleep(1);
}
}
if ($use_publicthread) {
$thread_payload = [
'name' => $thread_name !== '' ? $thread_name : 'Discussion - Opération',
'auto_archive_duration' => 1440,
'type' => 11,
];
$url = 'https://discord.com/api/v10/channels/' . rawurlencode($channel_id) . '/messages/' . rawurlencode($message_id) . '/threads';
$result = scdiscord_bot_request('POST', $url, $bot_token, $thread_payload);
$action_success = !empty($result['success']);
$last_http_code = (int) ($result['http_code'] ?? $last_http_code);
$details[] = [
'action' => 'thread',
'success' => $action_success,
'http_code' => $last_http_code,
'response' => (string) ($result['response'] ?? ''),
];
if (!$action_success) {
$failed = true;
}
}
return [
'success' => !$failed,
'http_code' => $failed ? $last_http_code : 200,
'response' => json_encode($details, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
'details' => $details,
];
}

139
db/scitemcustom.php Normal file
View File

@ -0,0 +1,139 @@
<?php
require_once __DIR__ . '/config.php';
function scitemcustom_column_exists(PDO $db, string $table, string $column): bool
{
$stmt = $db->query("SHOW COLUMNS FROM `{$table}` LIKE " . $db->quote($column));
return (bool) $stmt->fetch();
}
function scitemcustom_column_definition(PDO $db, string $table, string $column): ?array
{
$stmt = $db->query("SHOW COLUMNS FROM `{$table}` LIKE " . $db->quote($column));
$column_definition = $stmt->fetch(PDO::FETCH_ASSOC);
return $column_definition ?: null;
}
function scitemcustom_index_exists(PDO $db, string $table, string $index): bool
{
$stmt = $db->query("SHOW INDEX FROM `{$table}` WHERE Key_name = " . $db->quote($index));
return (bool) $stmt->fetch();
}
function scitemcustom_foreign_key_exists(PDO $db, string $table, string $constraint): bool
{
$stmt = $db->prepare(
"SELECT COUNT(*)
FROM information_schema.TABLE_CONSTRAINTS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = :table_name
AND CONSTRAINT_NAME = :constraint_name
AND CONSTRAINT_TYPE = 'FOREIGN KEY'"
);
$stmt->execute([
'table_name' => $table,
'constraint_name' => $constraint,
]);
return (int) $stmt->fetchColumn() > 0;
}
function scitemcustom_bootstrap(): void
{
static $scitemcustom_bootstrap_done = false;
if ($scitemcustom_bootstrap_done) {
return;
}
$db = db();
$db->exec(
"CREATE TABLE IF NOT EXISTS tbl_scitemcustom (
cl_scitemcustom_id INT(11) NOT NULL AUTO_INCREMENT,
cl_scitemcustom_owner_auth_id INT UNSIGNED NOT NULL,
cl_scitemcustom_obj_id INT(10) UNSIGNED NOT NULL,
cl_scitemcustom_created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (cl_scitemcustom_id),
KEY idx_scitemcustom_owner (cl_scitemcustom_owner_auth_id),
KEY idx_scitemcustom_obj (cl_scitemcustom_obj_id),
CONSTRAINT fk_scitemcustom_owner_auth FOREIGN KEY (cl_scitemcustom_owner_auth_id)
REFERENCES tbl_auth (cl_auth_id)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT fk_scitemcustom_obj FOREIGN KEY (cl_scitemcustom_obj_id)
REFERENCES tbl_scobjs (cl_scobjs_id)
ON DELETE CASCADE
ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
);
if (!scitemcustom_column_exists($db, 'tbl_scitemcustom', 'cl_scitemcustom_owner_auth_id')) {
$db->exec(
'ALTER TABLE tbl_scitemcustom
ADD COLUMN cl_scitemcustom_owner_auth_id INT UNSIGNED NULL AFTER cl_scitemcustom_id'
);
}
if (scitemcustom_index_exists($db, 'tbl_scitemcustom', 'uq_scitemcustom_obj')) {
$db->exec('ALTER TABLE tbl_scitemcustom DROP INDEX uq_scitemcustom_obj');
}
if (!scitemcustom_index_exists($db, 'tbl_scitemcustom', 'idx_scitemcustom_owner')) {
$db->exec(
'ALTER TABLE tbl_scitemcustom
ADD INDEX idx_scitemcustom_owner (cl_scitemcustom_owner_auth_id)'
);
}
if (scitemcustom_index_exists($db, 'tbl_scitemcustom', 'uq_scitemcustom_owner_obj')) {
$db->exec('ALTER TABLE tbl_scitemcustom DROP INDEX uq_scitemcustom_owner_obj');
}
if (!scitemcustom_foreign_key_exists($db, 'tbl_scitemcustom', 'fk_scitemcustom_owner_auth')) {
$db->exec(
'ALTER TABLE tbl_scitemcustom
ADD CONSTRAINT fk_scitemcustom_owner_auth FOREIGN KEY (cl_scitemcustom_owner_auth_id)
REFERENCES tbl_auth (cl_auth_id)
ON DELETE CASCADE
ON UPDATE CASCADE'
);
}
$db->exec(
"CREATE TABLE IF NOT EXISTS tbl_scitemcustomstat (
cl_scitemcustomstat_id INT(11) NOT NULL AUTO_INCREMENT,
cl_scitemcustomstat_itemcustom_id INT(11) NOT NULL,
cl_scitemcustomstat_stat_id INT(11) NOT NULL,
cl_scitemcustomstat_sign ENUM('+', '', '-') NOT NULL DEFAULT '+',
cl_scitemcustomstat_value DECIMAL(10,2) NOT NULL DEFAULT 0.00,
cl_scitemcustomstat_created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (cl_scitemcustomstat_id),
UNIQUE KEY uq_scitemcustomstat_item_stat (cl_scitemcustomstat_itemcustom_id, cl_scitemcustomstat_stat_id),
KEY idx_scitemcustomstat_item (cl_scitemcustomstat_itemcustom_id),
KEY idx_scitemcustomstat_stat (cl_scitemcustomstat_stat_id),
CONSTRAINT fk_scitemcustomstat_item FOREIGN KEY (cl_scitemcustomstat_itemcustom_id)
REFERENCES tbl_scitemcustom (cl_scitemcustom_id)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT fk_scitemcustomstat_stat FOREIGN KEY (cl_scitemcustomstat_stat_id)
REFERENCES tbl_scstatsitem (cl_scstatsitem_id)
ON DELETE CASCADE
ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
);
$sign_column = scitemcustom_column_definition($db, 'tbl_scitemcustomstat', 'cl_scitemcustomstat_sign');
if ($sign_column && ($sign_column['Type'] ?? '') !== "enum('+','','-')") {
$db->exec(
"ALTER TABLE tbl_scitemcustomstat
MODIFY COLUMN cl_scitemcustomstat_sign ENUM('+', '', '-') NOT NULL DEFAULT '+'"
);
}
$scitemcustom_bootstrap_done = true;
}

45
db/scitems.php Normal file
View File

@ -0,0 +1,45 @@
<?php
require_once __DIR__ . '/config.php';
function scitems_column_exists(PDO $db, string $table, string $column): bool
{
$stmt = $db->query("SHOW COLUMNS FROM `{$table}` LIKE " . $db->quote($column));
return (bool) $stmt->fetch();
}
function scitems_bootstrap(): void
{
static $bootstrapped = false;
if ($bootstrapped) {
return;
}
$db = db();
$db->exec(
"CREATE TABLE IF NOT EXISTS tbl_scobjs (
cl_scobjs_id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
cl_scobjs_name VARCHAR(255) NOT NULL,
cl_scobjs_type VARCHAR(100) DEFAULT NULL,
cl_scobjs_subtype VARCHAR(100) DEFAULT NULL,
cl_scobjs_uuid VARCHAR(100) NOT NULL,
cl_scobjs_rarity VARCHAR(10) DEFAULT '',
cl_scobjs_about TEXT DEFAULT NULL,
cl_scobjs_description TEXT DEFAULT NULL,
PRIMARY KEY (cl_scobjs_id),
UNIQUE KEY cl_scobjs_uuid (cl_scobjs_uuid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
);
if (!scitems_column_exists($db, 'tbl_scobjs', 'cl_scobjs_description')) {
$db->exec(
'ALTER TABLE tbl_scobjs
ADD COLUMN cl_scobjs_description TEXT DEFAULT NULL AFTER cl_scobjs_about'
);
}
$bootstrapped = true;
}

801
db/scmanutention.php Normal file
View File

@ -0,0 +1,801 @@
<?php
require_once __DIR__ . '/config.php';
function scmanutention_column_exists(PDO $db, string $table, string $column): bool
{
$stmt = $db->query("SHOW COLUMNS FROM `{$table}` LIKE " . $db->quote($column));
return (bool) $stmt->fetch();
}
function scmanutention_index_exists(PDO $db, string $table, string $index): bool
{
$stmt = $db->query("SHOW INDEX FROM `{$table}` WHERE Key_name = " . $db->quote($index));
return (bool) $stmt->fetch();
}
function scmanutention_foreign_key_exists(PDO $db, string $table, string $constraint): bool
{
$stmt = $db->prepare(
"SELECT COUNT(*)
FROM information_schema.TABLE_CONSTRAINTS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = :table_name
AND CONSTRAINT_NAME = :constraint_name
AND CONSTRAINT_TYPE = 'FOREIGN KEY'"
);
$stmt->execute([
'table_name' => $table,
'constraint_name' => $constraint,
]);
return (int) $stmt->fetchColumn() > 0;
}
function scmanutention_bootstrap(): void
{
static $bootstrapped = false;
if ($bootstrapped) {
return;
}
$db = db();
$db->exec(
"CREATE TABLE IF NOT EXISTS tbl_scmanutentions (
cl_scmanutention_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
cl_scmanutention_owner_auth_id INT UNSIGNED NOT NULL,
cl_scmanutention_title VARCHAR(190) NOT NULL,
cl_scmanutention_type VARCHAR(120) NOT NULL DEFAULT '',
cl_scmanutention_subtype VARCHAR(120) NOT NULL DEFAULT '',
cl_scmanutention_description TEXT DEFAULT NULL,
cl_scmanutention_share_token VARCHAR(64) NOT NULL,
cl_scmanutention_share_enabled TINYINT(1) NOT NULL DEFAULT 0,
cl_scmanutention_created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
cl_scmanutention_updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (cl_scmanutention_id),
UNIQUE KEY uq_scmanutention_share_token (cl_scmanutention_share_token),
KEY idx_scmanutention_owner (cl_scmanutention_owner_auth_id),
KEY idx_scmanutention_title (cl_scmanutention_title),
CONSTRAINT fk_scmanutention_owner_auth FOREIGN KEY (cl_scmanutention_owner_auth_id)
REFERENCES tbl_auth (cl_auth_id)
ON DELETE CASCADE
ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
);
if (!scmanutention_column_exists($db, 'tbl_scmanutentions', 'cl_scmanutention_owner_auth_id')) {
$db->exec(
'ALTER TABLE tbl_scmanutentions
ADD COLUMN cl_scmanutention_owner_auth_id INT UNSIGNED NULL AFTER cl_scmanutention_id'
);
}
if (!scmanutention_column_exists($db, 'tbl_scmanutentions', 'cl_scmanutention_type')) {
$db->exec(
"ALTER TABLE tbl_scmanutentions
ADD COLUMN cl_scmanutention_type VARCHAR(120) NOT NULL DEFAULT '' AFTER cl_scmanutention_title"
);
}
if (!scmanutention_column_exists($db, 'tbl_scmanutentions', 'cl_scmanutention_subtype')) {
$db->exec(
"ALTER TABLE tbl_scmanutentions
ADD COLUMN cl_scmanutention_subtype VARCHAR(120) NOT NULL DEFAULT '' AFTER cl_scmanutention_type"
);
}
if (!scmanutention_column_exists($db, 'tbl_scmanutentions', 'cl_scmanutention_description')) {
$db->exec(
'ALTER TABLE tbl_scmanutentions
ADD COLUMN cl_scmanutention_description TEXT NULL AFTER cl_scmanutention_subtype'
);
}
if (!scmanutention_column_exists($db, 'tbl_scmanutentions', 'cl_scmanutention_share_token')) {
$db->exec(
"ALTER TABLE tbl_scmanutentions
ADD COLUMN cl_scmanutention_share_token VARCHAR(64) NOT NULL DEFAULT '' AFTER cl_scmanutention_description"
);
}
if (!scmanutention_column_exists($db, 'tbl_scmanutentions', 'cl_scmanutention_share_enabled')) {
$db->exec(
'ALTER TABLE tbl_scmanutentions
ADD COLUMN cl_scmanutention_share_enabled TINYINT(1) NOT NULL DEFAULT 0 AFTER cl_scmanutention_share_token'
);
}
if (!scmanutention_index_exists($db, 'tbl_scmanutentions', 'idx_scmanutention_owner')) {
$db->exec(
'ALTER TABLE tbl_scmanutentions
ADD INDEX idx_scmanutention_owner (cl_scmanutention_owner_auth_id)'
);
}
if (!scmanutention_index_exists($db, 'tbl_scmanutentions', 'idx_scmanutention_title')) {
$db->exec(
'ALTER TABLE tbl_scmanutentions
ADD INDEX idx_scmanutention_title (cl_scmanutention_title)'
);
}
if (!scmanutention_index_exists($db, 'tbl_scmanutentions', 'uq_scmanutention_share_token')) {
$db->exec(
'ALTER TABLE tbl_scmanutentions
ADD UNIQUE KEY uq_scmanutention_share_token (cl_scmanutention_share_token)'
);
}
if (!scmanutention_foreign_key_exists($db, 'tbl_scmanutentions', 'fk_scmanutention_owner_auth')) {
$db->exec(
'ALTER TABLE tbl_scmanutentions
ADD CONSTRAINT fk_scmanutention_owner_auth FOREIGN KEY (cl_scmanutention_owner_auth_id)
REFERENCES tbl_auth (cl_auth_id)
ON DELETE CASCADE
ON UPDATE CASCADE'
);
}
$db->exec(
"CREATE TABLE IF NOT EXISTS tbl_scmanutentionitems (
cl_scmanutentionitem_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
cl_scmanutentionitem_manutention_id INT UNSIGNED NOT NULL,
cl_scmanutentionitem_source ENUM('base', 'custom') NOT NULL DEFAULT 'base',
cl_scmanutentionitem_scobjs_id INT UNSIGNED DEFAULT NULL,
cl_scmanutentionitem_scitemcustom_id INT(11) DEFAULT NULL,
cl_scmanutentionitem_quantity INT UNSIGNED NOT NULL DEFAULT 1,
cl_scmanutentionitem_extra_info TEXT DEFAULT NULL,
cl_scmanutentionitem_sort_order INT UNSIGNED NOT NULL DEFAULT 0,
cl_scmanutentionitem_created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
cl_scmanutentionitem_updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (cl_scmanutentionitem_id),
KEY idx_scmanutentionitem_sheet (cl_scmanutentionitem_manutention_id),
KEY idx_scmanutentionitem_scobjs (cl_scmanutentionitem_scobjs_id),
KEY idx_scmanutentionitem_scitemcustom (cl_scmanutentionitem_scitemcustom_id),
KEY idx_scmanutentionitem_sheet_sort (cl_scmanutentionitem_manutention_id, cl_scmanutentionitem_sort_order, cl_scmanutentionitem_id),
CONSTRAINT fk_scmanutentionitem_sheet FOREIGN KEY (cl_scmanutentionitem_manutention_id)
REFERENCES tbl_scmanutentions (cl_scmanutention_id)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT fk_scmanutentionitem_scobjs FOREIGN KEY (cl_scmanutentionitem_scobjs_id)
REFERENCES tbl_scobjs (cl_scobjs_id)
ON DELETE SET NULL
ON UPDATE CASCADE,
CONSTRAINT fk_scmanutentionitem_scitemcustom FOREIGN KEY (cl_scmanutentionitem_scitemcustom_id)
REFERENCES tbl_scitemcustom (cl_scitemcustom_id)
ON DELETE SET NULL
ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
);
if (!scmanutention_column_exists($db, 'tbl_scmanutentionitems', 'cl_scmanutentionitem_source')) {
$db->exec(
"ALTER TABLE tbl_scmanutentionitems
ADD COLUMN cl_scmanutentionitem_source ENUM('base', 'custom') NOT NULL DEFAULT 'base' AFTER cl_scmanutentionitem_manutention_id"
);
}
if (!scmanutention_column_exists($db, 'tbl_scmanutentionitems', 'cl_scmanutentionitem_scobjs_id')) {
$db->exec(
'ALTER TABLE tbl_scmanutentionitems
ADD COLUMN cl_scmanutentionitem_scobjs_id INT UNSIGNED NULL AFTER cl_scmanutentionitem_source'
);
}
if (!scmanutention_column_exists($db, 'tbl_scmanutentionitems', 'cl_scmanutentionitem_scitemcustom_id')) {
$db->exec(
'ALTER TABLE tbl_scmanutentionitems
ADD COLUMN cl_scmanutentionitem_scitemcustom_id INT(11) NULL AFTER cl_scmanutentionitem_scobjs_id'
);
}
if (!scmanutention_column_exists($db, 'tbl_scmanutentionitems', 'cl_scmanutentionitem_quantity')) {
$db->exec(
'ALTER TABLE tbl_scmanutentionitems
ADD COLUMN cl_scmanutentionitem_quantity INT UNSIGNED NOT NULL DEFAULT 1 AFTER cl_scmanutentionitem_scitemcustom_id'
);
}
if (!scmanutention_column_exists($db, 'tbl_scmanutentionitems', 'cl_scmanutentionitem_extra_info')) {
$db->exec(
'ALTER TABLE tbl_scmanutentionitems
ADD COLUMN cl_scmanutentionitem_extra_info TEXT NULL AFTER cl_scmanutentionitem_quantity'
);
}
if (!scmanutention_column_exists($db, 'tbl_scmanutentionitems', 'cl_scmanutentionitem_sort_order')) {
$db->exec(
'ALTER TABLE tbl_scmanutentionitems
ADD COLUMN cl_scmanutentionitem_sort_order INT UNSIGNED NOT NULL DEFAULT 0 AFTER cl_scmanutentionitem_extra_info'
);
}
if (!scmanutention_index_exists($db, 'tbl_scmanutentionitems', 'idx_scmanutentionitem_sheet')) {
$db->exec(
'ALTER TABLE tbl_scmanutentionitems
ADD INDEX idx_scmanutentionitem_sheet (cl_scmanutentionitem_manutention_id)'
);
}
if (!scmanutention_index_exists($db, 'tbl_scmanutentionitems', 'idx_scmanutentionitem_scobjs')) {
$db->exec(
'ALTER TABLE tbl_scmanutentionitems
ADD INDEX idx_scmanutentionitem_scobjs (cl_scmanutentionitem_scobjs_id)'
);
}
if (!scmanutention_index_exists($db, 'tbl_scmanutentionitems', 'idx_scmanutentionitem_scitemcustom')) {
$db->exec(
'ALTER TABLE tbl_scmanutentionitems
ADD INDEX idx_scmanutentionitem_scitemcustom (cl_scmanutentionitem_scitemcustom_id)'
);
}
if (!scmanutention_index_exists($db, 'tbl_scmanutentionitems', 'idx_scmanutentionitem_sheet_sort')) {
$db->exec(
'ALTER TABLE tbl_scmanutentionitems
ADD INDEX idx_scmanutentionitem_sheet_sort (cl_scmanutentionitem_manutention_id, cl_scmanutentionitem_sort_order, cl_scmanutentionitem_id)'
);
}
if (!scmanutention_foreign_key_exists($db, 'tbl_scmanutentionitems', 'fk_scmanutentionitem_sheet')) {
$db->exec(
'ALTER TABLE tbl_scmanutentionitems
ADD CONSTRAINT fk_scmanutentionitem_sheet FOREIGN KEY (cl_scmanutentionitem_manutention_id)
REFERENCES tbl_scmanutentions (cl_scmanutention_id)
ON DELETE CASCADE
ON UPDATE CASCADE'
);
}
if (!scmanutention_foreign_key_exists($db, 'tbl_scmanutentionitems', 'fk_scmanutentionitem_scobjs')) {
$db->exec(
'ALTER TABLE tbl_scmanutentionitems
ADD CONSTRAINT fk_scmanutentionitem_scobjs FOREIGN KEY (cl_scmanutentionitem_scobjs_id)
REFERENCES tbl_scobjs (cl_scobjs_id)
ON DELETE SET NULL
ON UPDATE CASCADE'
);
}
if (!scmanutention_foreign_key_exists($db, 'tbl_scmanutentionitems', 'fk_scmanutentionitem_scitemcustom')) {
$db->exec(
'ALTER TABLE tbl_scmanutentionitems
ADD CONSTRAINT fk_scmanutentionitem_scitemcustom FOREIGN KEY (cl_scmanutentionitem_scitemcustom_id)
REFERENCES tbl_scitemcustom (cl_scitemcustom_id)
ON DELETE SET NULL
ON UPDATE CASCADE'
);
}
$stmt_missing_tokens = $db->query(
"SELECT cl_scmanutention_id
FROM tbl_scmanutentions
WHERE cl_scmanutention_share_token = ''
OR cl_scmanutention_share_token IS NULL"
);
foreach ($stmt_missing_tokens->fetchAll(PDO::FETCH_COLUMN) as $sheet_id) {
$stmt_update = $db->prepare(
'UPDATE tbl_scmanutentions
SET cl_scmanutention_share_token = :token
WHERE cl_scmanutention_id = :id'
);
$stmt_update->execute([
'token' => scmanutention_generate_share_token(),
'id' => (int) $sheet_id,
]);
}
$bootstrapped = true;
}
function scmanutention_generate_share_token(int $length = 32): string
{
$length = max(16, min(64, $length));
return bin2hex(random_bytes((int) ceil($length / 2)));
}
function scmanutention_clean_text(?string $value): string
{
return trim((string) $value);
}
function scmanutention_normalize_quantity($value): int
{
$quantity = (int) $value;
if ($quantity <= 0) {
return 1;
}
return min($quantity, 999999);
}
function scmanutention_is_valid_source(string $value): bool
{
return in_array($value, ['base', 'custom'], true);
}
function scmanutention_escape_like(string $value): string
{
return strtr($value, [
'\\' => '\\\\',
'%' => '\\%',
'_' => '\\_',
]);
}
function scmanutention_sheet_share_url(string $token): string
{
$is_https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|| (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https')
|| ((string) ($_SERVER['SERVER_PORT'] ?? '') === '443');
$scheme = $is_https ? 'https' : 'http';
$host = trim((string) ($_SERVER['HTTP_HOST'] ?? ''));
if ($host === '') {
$host = '127.0.0.1';
}
return $scheme . '://' . $host . '/scmanutentionpublic.php?share=' . rawurlencode($token);
}
function scmanutention_find_owned_sheet(PDO $db, int $sheet_id, int $owner_auth_id): ?array
{
if ($sheet_id <= 0 || $owner_auth_id <= 0) {
return null;
}
$stmt = $db->prepare(
'SELECT *
FROM tbl_scmanutentions
WHERE cl_scmanutention_id = :id
AND cl_scmanutention_owner_auth_id = :owner_auth_id
LIMIT 1'
);
$stmt->execute([
'id' => $sheet_id,
'owner_auth_id' => $owner_auth_id,
]);
$row = $stmt->fetch();
return $row ?: null;
}
function scmanutention_find_public_sheet_by_token(PDO $db, string $share_token): ?array
{
$share_token = trim($share_token);
if ($share_token === '') {
return null;
}
$stmt = $db->prepare(
'SELECT m.*, COALESCE(NULLIF(TRIM(a.cl_auth_user), \'\'), \'Inconnu\') AS cl_scmanutention_owner_name
FROM tbl_scmanutentions m
LEFT JOIN tbl_auth a ON a.cl_auth_id = m.cl_scmanutention_owner_auth_id
WHERE m.cl_scmanutention_share_token = :share_token
AND m.cl_scmanutention_share_enabled = 1
LIMIT 1'
);
$stmt->execute(['share_token' => $share_token]);
$row = $stmt->fetch();
return $row ?: null;
}
function scmanutention_next_item_sort_order(PDO $db, int $sheet_id): int
{
$stmt = $db->prepare(
'SELECT COALESCE(MAX(cl_scmanutentionitem_sort_order), 0)
FROM tbl_scmanutentionitems
WHERE cl_scmanutentionitem_manutention_id = :sheet_id'
);
$stmt->execute(['sheet_id' => $sheet_id]);
return ((int) $stmt->fetchColumn()) + 1;
}
function scmanutention_reindex_items(PDO $db, int $sheet_id): void
{
if ($sheet_id <= 0) {
return;
}
$stmt = $db->prepare(
'SELECT cl_scmanutentionitem_id
FROM tbl_scmanutentionitems
WHERE cl_scmanutentionitem_manutention_id = :sheet_id
ORDER BY cl_scmanutentionitem_sort_order ASC, cl_scmanutentionitem_id ASC'
);
$stmt->execute(['sheet_id' => $sheet_id]);
$ids = $stmt->fetchAll(PDO::FETCH_COLUMN);
$position = 1;
$stmt_update = $db->prepare(
'UPDATE tbl_scmanutentionitems
SET cl_scmanutentionitem_sort_order = :sort_order
WHERE cl_scmanutentionitem_id = :id'
);
foreach ($ids as $id) {
$stmt_update->execute([
'sort_order' => $position++,
'id' => (int) $id,
]);
}
}
function scmanutention_find_owned_item(PDO $db, int $item_id, int $owner_auth_id): ?array
{
if ($item_id <= 0 || $owner_auth_id <= 0) {
return null;
}
$stmt = $db->prepare(
"SELECT
mi.*,
m.cl_scmanutention_owner_auth_id
FROM tbl_scmanutentionitems mi
INNER JOIN tbl_scmanutentions m ON m.cl_scmanutention_id = mi.cl_scmanutentionitem_manutention_id
WHERE mi.cl_scmanutentionitem_id = :item_id
AND m.cl_scmanutention_owner_auth_id = :owner_auth_id
LIMIT 1"
);
$stmt->execute([
'item_id' => $item_id,
'owner_auth_id' => $owner_auth_id,
]);
$row = $stmt->fetch();
return $row ?: null;
}
function scmanutention_validate_item_reference(PDO $db, int $owner_auth_id, string $source, int $scobjs_id, int $scitemcustom_id): ?array
{
if (!scmanutention_is_valid_source($source)) {
return null;
}
if ($source === 'base') {
if ($scobjs_id <= 0) {
return null;
}
$stmt = $db->prepare(
'SELECT cl_scobjs_id, cl_scobjs_name, cl_scobjs_type, cl_scobjs_subtype, cl_scobjs_uuid, cl_scobjs_rarity
FROM tbl_scobjs
WHERE cl_scobjs_id = :id
LIMIT 1'
);
$stmt->execute(['id' => $scobjs_id]);
$row = $stmt->fetch();
if (!$row) {
return null;
}
return [
'source' => 'base',
'scobjs_id' => (int) $row['cl_scobjs_id'],
'scitemcustom_id' => 0,
'name' => (string) $row['cl_scobjs_name'],
'type' => (string) ($row['cl_scobjs_type'] ?? ''),
'subtype' => (string) ($row['cl_scobjs_subtype'] ?? ''),
'uuid' => (string) ($row['cl_scobjs_uuid'] ?? ''),
'rarity' => (string) ($row['cl_scobjs_rarity'] ?? ''),
];
}
if ($scitemcustom_id <= 0 || $owner_auth_id <= 0) {
return null;
}
$stmt = $db->prepare(
"SELECT
c.cl_scitemcustom_id,
o.cl_scobjs_id,
o.cl_scobjs_name,
o.cl_scobjs_type,
o.cl_scobjs_subtype,
o.cl_scobjs_uuid,
o.cl_scobjs_rarity
FROM tbl_scitemcustom c
INNER JOIN tbl_scobjs o ON o.cl_scobjs_id = c.cl_scitemcustom_obj_id
WHERE c.cl_scitemcustom_id = :itemcustom_id
AND c.cl_scitemcustom_owner_auth_id = :owner_auth_id
LIMIT 1"
);
$stmt->execute([
'itemcustom_id' => $scitemcustom_id,
'owner_auth_id' => $owner_auth_id,
]);
$row = $stmt->fetch();
if (!$row) {
return null;
}
return [
'source' => 'custom',
'scobjs_id' => (int) $row['cl_scobjs_id'],
'scitemcustom_id' => (int) $row['cl_scitemcustom_id'],
'name' => (string) $row['cl_scobjs_name'],
'type' => (string) ($row['cl_scobjs_type'] ?? ''),
'subtype' => (string) ($row['cl_scobjs_subtype'] ?? ''),
'uuid' => (string) ($row['cl_scobjs_uuid'] ?? ''),
'rarity' => (string) ($row['cl_scobjs_rarity'] ?? ''),
];
}
function scmanutention_search_available_items(PDO $db, int $owner_auth_id, string $query, ?int $limit = 25, int $offset = 0): array
{
$query = trim($query);
if ($query === '') {
return [];
}
$escaped = scmanutention_escape_like($query);
$exact = $escaped;
$prefix = $escaped . '%';
$contains = '%' . $escaped . '%';
$limit_clause = '';
$offset = max(0, $offset);
if ($limit !== null && $limit > 0) {
$limit = max(1, min(100, $limit));
$limit_clause = ' LIMIT ' . (int) $limit . ' OFFSET ' . (int) $offset;
}
$sql = "
SELECT *
FROM (
SELECT
CONCAT('base:', o.cl_scobjs_id) AS result_key,
'base' AS result_source,
o.cl_scobjs_id AS result_scobjs_id,
NULL AS result_scitemcustom_id,
o.cl_scobjs_name AS result_name,
COALESCE(o.cl_scobjs_type, '') AS result_type,
COALESCE(o.cl_scobjs_subtype, '') AS result_subtype,
COALESCE(o.cl_scobjs_uuid, '') AS result_uuid,
COALESCE(o.cl_scobjs_rarity, '') AS result_rarity
FROM tbl_scobjs o
WHERE (
o.cl_scobjs_name LIKE :contains_name_base
OR o.cl_scobjs_type LIKE :contains_type_base
OR o.cl_scobjs_subtype LIKE :contains_subtype_base
OR o.cl_scobjs_uuid LIKE :contains_uuid_base
)
UNION ALL
SELECT
CONCAT('custom:', c.cl_scitemcustom_id) AS result_key,
'custom' AS result_source,
o.cl_scobjs_id AS result_scobjs_id,
c.cl_scitemcustom_id AS result_scitemcustom_id,
o.cl_scobjs_name AS result_name,
COALESCE(o.cl_scobjs_type, '') AS result_type,
COALESCE(o.cl_scobjs_subtype, '') AS result_subtype,
COALESCE(o.cl_scobjs_uuid, '') AS result_uuid,
COALESCE(o.cl_scobjs_rarity, '') AS result_rarity
FROM tbl_scitemcustom c
INNER JOIN tbl_scobjs o ON o.cl_scobjs_id = c.cl_scitemcustom_obj_id
WHERE c.cl_scitemcustom_owner_auth_id = :owner_auth_id
AND (
o.cl_scobjs_name LIKE :contains_name_custom
OR o.cl_scobjs_type LIKE :contains_type_custom
OR o.cl_scobjs_subtype LIKE :contains_subtype_custom
OR o.cl_scobjs_uuid LIKE :contains_uuid_custom
)
) search_results
ORDER BY
CASE
WHEN result_name = :exact_name THEN 0
WHEN result_name LIKE :prefix_name THEN 1
WHEN result_source = 'custom' THEN 2
WHEN result_uuid = :exact_uuid THEN 3
WHEN result_uuid LIKE :prefix_uuid THEN 4
WHEN result_type LIKE :prefix_type THEN 5
WHEN result_subtype LIKE :prefix_subtype THEN 6
ELSE 7
END ASC,
CHAR_LENGTH(result_name) ASC,
result_name ASC,
result_key ASC
{$limit_clause}";
$stmt = $db->prepare($sql);
$stmt->execute([
'owner_auth_id' => $owner_auth_id,
'contains_name_base' => $contains,
'contains_type_base' => $contains,
'contains_subtype_base' => $contains,
'contains_uuid_base' => $contains,
'contains_name_custom' => $contains,
'contains_type_custom' => $contains,
'contains_subtype_custom' => $contains,
'contains_uuid_custom' => $contains,
'exact_name' => $exact,
'prefix_name' => $prefix,
'exact_uuid' => $exact,
'prefix_uuid' => $prefix,
'prefix_type' => $prefix,
'prefix_subtype' => $prefix,
]);
return $stmt->fetchAll() ?: [];
}
function scmanutention_sortable_item_name(array $item_row): string
{
$name = (string) ((($item_row['cl_scmanutentionitem_source'] ?? '') === 'custom')
? ($item_row['cl_scmanutentionitem_custom_name'] ?? '')
: ($item_row['cl_scmanutentionitem_base_name'] ?? ''));
$name = trim($name);
if ($name === '') {
return '';
}
if (function_exists('iconv')) {
$ascii_name = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $name);
if ($ascii_name !== false) {
$name = $ascii_name;
}
}
return function_exists('mb_strtolower')
? mb_strtolower($name, 'UTF-8')
: strtolower($name);
}
function scmanutention_fetch_items(PDO $db, int $sheet_id): array
{
if ($sheet_id <= 0) {
return [];
}
scmanutention_reindex_items($db, $sheet_id);
$stmt = $db->prepare(
"SELECT
mi.*,
bo.cl_scobjs_name AS cl_scmanutentionitem_base_name,
bo.cl_scobjs_type AS cl_scmanutentionitem_base_type,
bo.cl_scobjs_subtype AS cl_scmanutentionitem_base_subtype,
bo.cl_scobjs_uuid AS cl_scmanutentionitem_base_uuid,
bo.cl_scobjs_rarity AS cl_scmanutentionitem_base_rarity,
co.cl_scitemcustom_id AS cl_scmanutentionitem_custom_ref_id,
oo.cl_scobjs_name AS cl_scmanutentionitem_custom_name,
oo.cl_scobjs_type AS cl_scmanutentionitem_custom_type,
oo.cl_scobjs_subtype AS cl_scmanutentionitem_custom_subtype,
oo.cl_scobjs_uuid AS cl_scmanutentionitem_custom_uuid,
oo.cl_scobjs_rarity AS cl_scmanutentionitem_custom_rarity
FROM tbl_scmanutentionitems mi
LEFT JOIN tbl_scobjs bo ON bo.cl_scobjs_id = mi.cl_scmanutentionitem_scobjs_id
LEFT JOIN tbl_scitemcustom co ON co.cl_scitemcustom_id = mi.cl_scmanutentionitem_scitemcustom_id
LEFT JOIN tbl_scobjs oo ON oo.cl_scobjs_id = co.cl_scitemcustom_obj_id
WHERE mi.cl_scmanutentionitem_manutention_id = :sheet_id
ORDER BY mi.cl_scmanutentionitem_sort_order ASC, mi.cl_scmanutentionitem_id ASC"
);
$stmt->execute(['sheet_id' => $sheet_id]);
$items = $stmt->fetchAll() ?: [];
foreach ($items as $index => &$item_row) {
$item_row['__alpha_sort_name'] = scmanutention_sortable_item_name($item_row);
$item_row['__alpha_sort_index'] = $index;
}
unset($item_row);
usort($items, static function (array $left, array $right): int {
$name_compare = strnatcasecmp((string) ($left['__alpha_sort_name'] ?? ''), (string) ($right['__alpha_sort_name'] ?? ''));
if ($name_compare !== 0) {
return $name_compare;
}
$sort_order_compare = ((int) ($left['cl_scmanutentionitem_sort_order'] ?? 0)) <=> ((int) ($right['cl_scmanutentionitem_sort_order'] ?? 0));
if ($sort_order_compare !== 0) {
return $sort_order_compare;
}
$id_compare = ((int) ($left['cl_scmanutentionitem_id'] ?? 0)) <=> ((int) ($right['cl_scmanutentionitem_id'] ?? 0));
if ($id_compare !== 0) {
return $id_compare;
}
return ((int) ($left['__alpha_sort_index'] ?? 0)) <=> ((int) ($right['__alpha_sort_index'] ?? 0));
});
foreach ($items as &$item_row) {
unset($item_row['__alpha_sort_name'], $item_row['__alpha_sort_index']);
}
unset($item_row);
return $items;
}
function scmanutention_fetch_custom_stats_map(PDO $db, array $item_rows): array
{
$custom_ids = [];
foreach ($item_rows as $row) {
if (($row['cl_scmanutentionitem_source'] ?? '') === 'custom' && !empty($row['cl_scmanutentionitem_scitemcustom_id'])) {
$custom_ids[] = (int) $row['cl_scmanutentionitem_scitemcustom_id'];
}
}
$custom_ids = array_values(array_unique(array_filter($custom_ids)));
if ($custom_ids === []) {
return [];
}
$placeholders = implode(',', array_fill(0, count($custom_ids), '?'));
$stmt = $db->prepare(
"SELECT
cs.cl_scitemcustomstat_itemcustom_id,
st.cl_scstatsitem_name,
st.cl_scstatsitem_unit,
cs.cl_scitemcustomstat_sign,
cs.cl_scitemcustomstat_value
FROM tbl_scitemcustomstat cs
INNER JOIN tbl_scstatsitem st ON st.cl_scstatsitem_id = cs.cl_scitemcustomstat_stat_id
WHERE cs.cl_scitemcustomstat_itemcustom_id IN ({$placeholders})
ORDER BY st.cl_scstatsitem_name ASC, cs.cl_scitemcustomstat_id ASC"
);
$stmt->execute($custom_ids);
$stats_map = [];
foreach ($stmt->fetchAll() as $row) {
$itemcustom_id = (int) $row['cl_scitemcustomstat_itemcustom_id'];
if (!isset($stats_map[$itemcustom_id])) {
$stats_map[$itemcustom_id] = [];
}
$stats_map[$itemcustom_id][] = $row;
}
return $stats_map;
}
function scmanutention_fetch_custom_stats_preview_map(PDO $db, array $custom_ids): array
{
$custom_ids = array_values(array_unique(array_map('intval', array_filter($custom_ids))));
if ($custom_ids === []) {
return [];
}
$placeholders = implode(',', array_fill(0, count($custom_ids), '?'));
$stmt = $db->prepare(
"SELECT
cs.cl_scitemcustomstat_itemcustom_id,
st.cl_scstatsitem_name,
st.cl_scstatsitem_unit,
cs.cl_scitemcustomstat_sign,
cs.cl_scitemcustomstat_value
FROM tbl_scitemcustomstat cs
INNER JOIN tbl_scstatsitem st ON st.cl_scstatsitem_id = cs.cl_scitemcustomstat_stat_id
WHERE cs.cl_scitemcustomstat_itemcustom_id IN ({$placeholders})
ORDER BY st.cl_scstatsitem_name ASC, cs.cl_scitemcustomstat_id ASC"
);
$stmt->execute($custom_ids);
$stats_map = [];
foreach ($stmt->fetchAll() as $row) {
$itemcustom_id = (int) $row['cl_scitemcustomstat_itemcustom_id'];
if (!isset($stats_map[$itemcustom_id])) {
$stats_map[$itemcustom_id] = [];
}
$stats_map[$itemcustom_id][] = $row;
}
return $stats_map;
}

32
db/scstatsitem.php Normal file
View File

@ -0,0 +1,32 @@
<?php
require_once __DIR__ . '/config.php';
function scstatsitem_bootstrap(): void
{
static $bootstrapped = false;
if ($bootstrapped) {
return;
}
$db = db();
$db->exec(
"CREATE TABLE IF NOT EXISTS tbl_scstatsitem (
cl_scstatsitem_id INT(11) NOT NULL AUTO_INCREMENT,
cl_scstatsitem_name VARCHAR(255) NOT NULL,
cl_scstatsitem_unit VARCHAR(10) NOT NULL DEFAULT '%',
cl_scstatsitem_created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (cl_scstatsitem_id),
UNIQUE KEY uq_scstatsitem_name (cl_scstatsitem_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci"
);
$columns_stmt = $db->query("SHOW COLUMNS FROM tbl_scstatsitem LIKE 'cl_scstatsitem_unit'");
$has_unit = (bool) $columns_stmt->fetch();
if (!$has_unit) {
$db->exec("ALTER TABLE tbl_scstatsitem ADD COLUMN cl_scstatsitem_unit VARCHAR(10) NOT NULL DEFAULT '%' AFTER cl_scstatsitem_name");
}
$bootstrapped = true;
}

File diff suppressed because it is too large Load Diff

3355
index.php

File diff suppressed because it is too large Load Diff

14
info.php Normal file
View File

@ -0,0 +1,14 @@
<?php
$config = [
'upload_max_filesize',
'post_max_size',
'max_execution_time',
'max_input_time',
'session.gc_maxlifetime',
'session.save_path',
'session.cookie_lifetime'
];
foreach ($config as $c) {
echo "$c: " . ini_get($c) . "\n";
}
?>

134
js/auth.js Normal file
View File

@ -0,0 +1,134 @@
(function () {
var loginForm = document.querySelector('.js-login-form');
var accountPanel = document.getElementById('accountPanel');
var accountLabel = document.getElementById('accountLabel');
var accountActions = document.getElementById('accountActions');
var adminLink = document.getElementById('adminLink');
var logoutLink = document.getElementById('logoutLink');
var loginStatus = document.getElementById('loginStatus');
var loginModal = document.getElementById('modal-Login');
var overlay = document.querySelector('.md-overlay');
if (!loginForm || !accountPanel || !accountLabel || !accountActions || !logoutLink) {
return;
}
function setStatus(message, isError) {
if (!loginStatus) {
return;
}
loginStatus.textContent = message || '';
loginStatus.classList.toggle('is-error', !!isError);
loginStatus.classList.toggle('is-success', !isError && !!message);
}
function closeModal() {
if (loginModal) {
loginModal.classList.remove('md-show');
}
if (document.documentElement) {
document.documentElement.classList.remove('md-perspective');
}
if (overlay) {
overlay.removeEventListener('click', closeModal);
}
}
function renderAuthenticated(user, role, adminUrl, logoutUrl) {
accountPanel.classList.add('is-authenticated');
accountPanel.classList.remove('md-trigger');
accountPanel.removeAttribute('data-modal');
accountLabel.textContent = 'Bonjour, ' + user;
accountActions.hidden = false;
if (role === 'admin' && adminUrl) {
adminLink.href = adminUrl;
adminLink.hidden = false;
} else {
adminLink.hidden = true;
}
if (logoutUrl) {
logoutLink.href = logoutUrl;
}
}
function renderLoggedOut(defaultLabel) {
accountPanel.classList.remove('is-authenticated');
accountPanel.classList.add('md-trigger');
accountPanel.setAttribute('data-modal', 'modal-Login');
accountLabel.textContent = defaultLabel;
accountActions.hidden = true;
adminLink.hidden = true;
}
accountPanel.addEventListener('click', function (event) {
if (!accountPanel.classList.contains('is-authenticated')) {
return;
}
event.stopImmediatePropagation();
}, true);
loginForm.addEventListener('submit', function (event) {
event.preventDefault();
setStatus('', false);
var formData = new FormData(loginForm);
fetch('login.php', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
})
.then(function (response) {
return response.json().then(function (payload) {
return {
ok: response.ok,
payload: payload
};
});
})
.then(function (result) {
if (!result.ok || !result.payload.success) {
throw new Error(result.payload.message || 'Connexion impossible.');
}
renderAuthenticated(result.payload.user, result.payload.role, result.payload.adminUrl, result.payload.logoutUrl);
setStatus(result.payload.message || 'Connexion réussie.', false);
loginForm.reset();
window.setTimeout(function () {
closeModal();
window.location.reload();
}, 250);
})
.catch(function (error) {
setStatus(error.message || 'Connexion impossible.', true);
});
});
logoutLink.addEventListener('click', function (event) {
event.preventDefault();
fetch(logoutLink.href, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(function (response) {
return response.json();
})
.then(function () {
renderLoggedOut(accountPanel.dataset.loginLabel || 'Connexion');
setStatus('', false);
window.location.reload();
})
.catch(function () {
window.location.href = logoutLink.href;
});
});
})();

89
login.php Normal file
View File

@ -0,0 +1,89 @@
<?php
require_once __DIR__ . '/db/auth.php';
auth_start_session();
auth_bootstrap();
header('Content-Type: application/json; charset=UTF-8');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode([
'success' => false,
'message' => 'Méthode non autorisée.',
], JSON_UNESCAPED_UNICODE);
exit;
}
$submitted_cl_auth_user = trim((string) ($_POST['cl_auth_user'] ?? ''));
$submitted_cl_auth_pass = (string) ($_POST['cl_auth_pass'] ?? '');
if ($submitted_cl_auth_user === '' || $submitted_cl_auth_pass === '') {
http_response_code(422);
echo json_encode([
'success' => false,
'message' => 'Identifiants incomplets.',
], JSON_UNESCAPED_UNICODE);
exit;
}
$stmt_tbl_auth = db()->prepare(
'SELECT cl_auth_id, cl_auth_user, cl_auth_pass, cl_auth_right
FROM tbl_auth
WHERE cl_auth_user = :cl_auth_user
LIMIT 1'
);
$stmt_tbl_auth->execute([
'cl_auth_user' => $submitted_cl_auth_user,
]);
$tbl_auth = $stmt_tbl_auth->fetch();
if (!$tbl_auth) {
http_response_code(401);
echo json_encode([
'success' => false,
'message' => 'Identifiants invalides.',
], JSON_UNESCAPED_UNICODE);
exit;
}
$cl_auth_id = (int) $tbl_auth['cl_auth_id'];
$cl_auth_user = (string) $tbl_auth['cl_auth_user'];
$cl_auth_pass = (string) $tbl_auth['cl_auth_pass'];
$cl_auth_right = (string) $tbl_auth['cl_auth_right'];
unset($cl_auth_id);
if (!password_verify($submitted_cl_auth_pass, $cl_auth_pass)) {
http_response_code(401);
echo json_encode([
'success' => false,
'message' => 'Identifiants invalides.',
], JSON_UNESCAPED_UNICODE);
exit;
}
if (password_needs_rehash($cl_auth_pass, PASSWORD_DEFAULT)) {
$rehash_cl_auth_pass = password_hash($submitted_cl_auth_pass, PASSWORD_DEFAULT);
$stmt_update_password = db()->prepare(
'UPDATE tbl_auth SET cl_auth_pass = :cl_auth_pass WHERE cl_auth_user = :cl_auth_user'
);
$stmt_update_password->execute([
'cl_auth_pass' => $rehash_cl_auth_pass,
'cl_auth_user' => $cl_auth_user,
]);
}
session_regenerate_id(true);
$_SESSION['user'] = $cl_auth_user;
$_SESSION['role'] = $cl_auth_right;
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
echo json_encode([
'success' => true,
'message' => 'Connexion réussie.',
'user' => $cl_auth_user,
'role' => $cl_auth_right,
'adminUrl' => 'admin.php',
'logoutUrl' => 'logout.php',
], JSON_UNESCAPED_UNICODE);

36
logout.php Normal file
View File

@ -0,0 +1,36 @@
<?php
require_once __DIR__ . '/db/auth.php';
auth_start_session();
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$session_cookie_params = session_get_cookie_params();
setcookie(
session_name(),
'',
time() - 42000,
$session_cookie_params['path'],
$session_cookie_params['domain'],
$session_cookie_params['secure'],
$session_cookie_params['httponly']
);
}
session_destroy();
$is_ajax = isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest';
if ($is_ajax) {
header('Content-Type: application/json; charset=UTF-8');
echo json_encode([
'success' => true,
'message' => 'Déconnexion effectuée.',
], JSON_UNESCAPED_UNICODE);
exit;
}
header('Location: index.php');
exit;

590
sccharacter.php Normal file
View File

@ -0,0 +1,590 @@
<?php
require_once __DIR__ . '/db/auth.php';
require_once __DIR__ . '/db/scstatsitem.php';
require_once __DIR__ . '/db/scitemcustom.php';
require_once __DIR__ . '/db/sccharacters.php';
auth_start_session();
auth_bootstrap();
scstatsitem_bootstrap();
scitemcustom_bootstrap();
sccharacters_bootstrap();
$db = db();
$share_token = trim((string) ($_GET['share'] ?? ''));
$character = null;
$character_items = [];
$custom_stats_by_itemcustom = [];
$character_org_tag = '';
$character_has_player_handle = false;
if ($share_token !== '') {
$stmt_character = $db->prepare(
'SELECT c.*, COALESCE(NULLIF(TRIM(a.cl_auth_user), \'\'), \'Inconnu\') AS cl_sccharacter_creator_name
FROM tbl_sccharacters c
LEFT JOIN tbl_auth a ON a.cl_auth_id = c.cl_sccharacter_owner_auth_id
WHERE c.cl_sccharacter_share_token = :share_token
AND c.cl_sccharacter_share_enabled = 1
LIMIT 1'
);
$stmt_character->execute(['share_token' => $share_token]);
$character = $stmt_character->fetch() ?: null;
}
if ($character) {
$character_org_tag = sccharacters_resolve_org_tag($character);
$character_has_player_handle = sccharacters_has_player_handle($character);
sccharacters_reindex_character_items($db, (int) $character['cl_sccharacter_id']);
$stmt_items = $db->prepare(
"SELECT
ci.*,
bo.cl_scobjs_name AS cl_sccharacteritem_base_name,
bo.cl_scobjs_type AS cl_sccharacteritem_base_type,
bo.cl_scobjs_subtype AS cl_sccharacteritem_base_subtype,
bo.cl_scobjs_uuid AS cl_sccharacteritem_base_uuid,
oo.cl_scobjs_name AS cl_sccharacteritem_custom_name,
oo.cl_scobjs_type AS cl_sccharacteritem_custom_type,
oo.cl_scobjs_subtype AS cl_sccharacteritem_custom_subtype,
oo.cl_scobjs_uuid AS cl_sccharacteritem_custom_uuid
FROM tbl_sccharacteritems ci
LEFT JOIN tbl_scobjs bo ON bo.cl_scobjs_id = ci.cl_sccharacteritem_scobjs_id
LEFT JOIN tbl_scitemcustom co ON co.cl_scitemcustom_id = ci.cl_sccharacteritem_scitemcustom_id
LEFT JOIN tbl_scobjs oo ON oo.cl_scobjs_id = co.cl_scitemcustom_obj_id
WHERE ci.cl_sccharacteritem_character_id = :character_id
ORDER BY ci.cl_sccharacteritem_sort_order ASC, ci.cl_sccharacteritem_id ASC"
);
$stmt_items->execute(['character_id' => (int) $character['cl_sccharacter_id']]);
$character_items = $stmt_items->fetchAll();
$custom_item_ids = [];
foreach ($character_items as $row) {
if (($row['cl_sccharacteritem_source'] ?? '') === 'custom' && !empty($row['cl_sccharacteritem_scitemcustom_id'])) {
$custom_item_ids[] = (int) $row['cl_sccharacteritem_scitemcustom_id'];
}
}
$custom_item_ids = array_values(array_unique(array_filter($custom_item_ids)));
if ($custom_item_ids !== []) {
$placeholders = implode(',', array_fill(0, count($custom_item_ids), '?'));
$stmt_stats = $db->prepare(
"SELECT
cs.cl_scitemcustomstat_itemcustom_id,
st.cl_scstatsitem_name,
st.cl_scstatsitem_unit,
cs.cl_scitemcustomstat_sign,
cs.cl_scitemcustomstat_value
FROM tbl_scitemcustomstat cs
INNER JOIN tbl_scstatsitem st ON st.cl_scstatsitem_id = cs.cl_scitemcustomstat_stat_id
WHERE cs.cl_scitemcustomstat_itemcustom_id IN ({$placeholders})
ORDER BY st.cl_scstatsitem_name ASC, cs.cl_scitemcustomstat_id ASC"
);
$stmt_stats->execute($custom_item_ids);
foreach ($stmt_stats->fetchAll() as $stat_row) {
$itemcustom_id = (int) $stat_row['cl_scitemcustomstat_itemcustom_id'];
if (!isset($custom_stats_by_itemcustom[$itemcustom_id])) {
$custom_stats_by_itemcustom[$itemcustom_id] = [];
}
$custom_stats_by_itemcustom[$itemcustom_id][] = $stat_row;
}
}
} else {
http_response_code(404);
}
$item_category_options = sccharacters_item_category_options();
$character_category_order = $character
? sccharacters_character_category_order($character)
: sccharacters_default_category_order();
$character_items_by_category = [];
foreach ($character_items as $item_row) {
$is_custom = ($item_row['cl_sccharacteritem_source'] ?? '') === 'custom';
$type = $is_custom
? (string) ($item_row['cl_sccharacteritem_custom_type'] ?? '')
: (string) ($item_row['cl_sccharacteritem_base_type'] ?? '');
$subtype = $is_custom
? (string) ($item_row['cl_sccharacteritem_custom_subtype'] ?? '')
: (string) ($item_row['cl_sccharacteritem_base_subtype'] ?? '');
$category_key = sccharacters_resolve_item_category(
(string) ($item_row['cl_sccharacteritem_slot'] ?? ''),
$type,
$subtype
);
if (!isset($character_items_by_category[$category_key])) {
$character_items_by_category[$category_key] = [];
}
$character_items_by_category[$category_key][] = $item_row;
}
$character_items_by_category = sccharacters_sort_items_by_category_order(
$character_items_by_category,
$character_category_order
);
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo htmlspecialchars($character ? ((string) $character['cl_sccharacter_name'] . ' | Personnage partagé') : 'Personnage introuvable', ENT_QUOTES, 'UTF-8'); ?></title>
<style>
:root {
--primary: #a29b78;
--primary-soft: rgba(162, 155, 120, 0.18);
--primary-border: rgba(162, 155, 120, 0.3);
--bg: #080a0f;
--card: rgba(20, 24, 33, 0.88);
--text-main: #ece9df;
--text-soft: rgba(236, 233, 223, 0.72);
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background-color: #050608;
color: var(--text-main);
font-family: Arial, Helvetica, sans-serif;
position: relative;
}
body::before {
content: '';
position: fixed;
inset: 0;
background-image:
linear-gradient(rgba(5, 8, 12, 0.58), rgba(5, 8, 12, 0.72)),
url('https://robertsspaceindustries.com/media/1vllgn95062syr/background_blur/REACT-Background.jpg');
background-position: center center;
background-repeat: no-repeat;
background-size: cover;
z-index: -2;
transform: scale(1.08);
transform-origin: center center;
}
body::after {
content: '';
position: fixed;
inset: 0;
background: radial-gradient(circle at top right, rgba(29, 34, 52, 0.22) 0%, rgba(10, 13, 19, 0.18) 55%, rgba(5, 6, 8, 0.1) 100%);
z-index: -1;
pointer-events: none;
}
.page {
max-width: 1180px;
margin: 0 auto;
padding: 2rem 1rem 3rem;
position: relative;
z-index: 1;
}
.hero,
.card,
.message {
background: var(--card);
border: 1px solid var(--primary-border);
border-radius: 20px;
box-shadow: 0 22px 50px rgba(0,0,0,0.28);
backdrop-filter: blur(10px);
}
.hero {
padding: 1.4rem;
display: grid;
grid-template-columns: 120px minmax(0, 1fr);
gap: 1.2rem;
margin-bottom: 1.3rem;
}
.avatar,
.avatar-fallback {
width: 120px;
height: 120px;
border-radius: 28px;
object-fit: cover;
background: linear-gradient(145deg, rgba(162, 155, 120, 0.3), rgba(255,255,255,0.08));
border: 1px solid rgba(255,255,255,0.08);
}
.avatar-fallback {
display: flex;
align-items: center;
justify-content: center;
font-size: 2.2rem;
color: var(--primary);
font-weight: 700;
}
h1 { margin: 0 0 0.65rem; font-size: 2rem; }
p { line-height: 1.6; }
.meta,
.stats,
.tags {
display: flex;
gap: 0.55rem;
flex-wrap: wrap;
align-items: center;
}
.identity-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.7rem;
margin-bottom: 0.8rem;
}
.identity-item {
display: flex;
flex-direction: column;
gap: 0.24rem;
padding: 0.76rem 0.84rem;
border-radius: 14px;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.1);
}
.identity-label {
font-size: 0.7rem;
letter-spacing: 0.09em;
text-transform: uppercase;
color: var(--text-soft);
}
.identity-value {
font-size: 0.96rem;
font-weight: 600;
color: #f6f7fb;
line-height: 1.35;
word-break: break-word;
}
.meta {
margin-top: 0.2rem;
}
.tag {
display: inline-flex;
align-items: center;
padding: 0.34rem 0.62rem;
border-radius: 999px;
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.08);
font-size: 0.84rem;
}
.tag-primary {
background: var(--primary-soft);
border-color: rgba(162, 155, 120, 0.35);
color: #f6eebf;
}
.muted { color: var(--text-soft); }
.equipment-sections {
display: flex;
flex-direction: column;
gap: 2.2rem;
}
.equipment-section {
display: flex;
flex-direction: column;
gap: 0.8rem;
}
.equipment-section-head {
display: flex;
align-items: center;
gap: 0.8rem;
}
.equipment-section-title {
margin: 0;
color: var(--primary);
letter-spacing: 0.08em;
text-transform: uppercase;
font-size: 1rem;
display: flex;
align-items: center;
gap: 0.85rem;
width: 100%;
}
.equipment-section-title::after {
content: '';
flex: 1 1 auto;
height: 2px;
background: currentColor;
border-radius: 999px;
opacity: 0.95;
}
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
}
.card {
padding: 1rem;
display: flex;
gap: 0.95rem;
align-items: flex-start;
position: relative;
z-index: 0;
overflow: visible;
}
.card:hover,
.card:focus-within {
z-index: 2000;
}
.preview-container {
position: relative;
width: 68px;
height: 68px;
flex: 0 0 68px;
cursor: zoom-in;
}
.thumb,
.thumb-fallback {
width: 68px;
height: 68px;
border-radius: 18px;
object-fit: cover;
flex: 0 0 68px;
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.08);
display: block;
}
.thumb-fallback {
display: flex;
align-items: center;
justify-content: center;
color: var(--primary);
font-size: 1.2rem;
}
.preview-floating {
visibility: hidden;
opacity: 0;
position: absolute;
top: -10px;
left: 82px;
z-index: 50;
padding: 5px;
background: var(--card);
border: 1px solid rgba(162, 155, 120, 0.4);
border-radius: 12px;
box-shadow: 0 14px 32px rgba(0,0,0,0.45);
backdrop-filter: blur(12px);
transition: opacity 0.2s ease, visibility 0.2s ease;
pointer-events: none;
}
.preview-floating img {
width: 260px;
height: 260px;
object-fit: contain;
display: block;
border-radius: 8px;
}
.preview-container:hover .preview-floating,
.preview-container:focus-within .preview-floating {
visibility: visible;
opacity: 1;
}
.card-body { min-width: 0; }
.card-body h3 { margin: 0 0 0.45rem; font-size: 1.05rem; }
.stats {
margin-top: 0.7rem;
display: flex;
gap: 0.55rem;
flex-wrap: wrap;
align-items: stretch;
padding: 0;
border: 0;
background: transparent;
box-shadow: none;
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
.stat {
display: inline-flex;
align-items: center;
min-height: 24px;
padding: 0.2rem 0.55rem;
border-radius: 999px;
background: rgba(22, 78, 45, 0.14);
border: 1px solid rgba(90, 255, 150, 0.42);
box-shadow: none;
font-size: 0.68rem;
font-weight: 600;
line-height: 1.15;
color: #d7ffe6;
}
.section-title {
margin: 1.2rem 0 0.8rem;
color: var(--primary);
letter-spacing: 0.08em;
text-transform: uppercase;
font-size: 1rem;
}
.message {
padding: 1.4rem;
text-align: center;
}
@media (max-width: 860px) {
.hero,
.grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="page">
<?php if (!$character): ?>
<div class="message">
<h1>Personnage introuvable</h1>
<p class="muted">Ce lien public est invalide, désactivé ou nest plus disponible.</p>
</div>
<?php else: ?>
<?php
$avatar = trim((string) ($character['cl_sccharacter_avatar_url'] ?? ''));
$initial = function_exists('mb_substr')
? mb_strtoupper(mb_substr((string) $character['cl_sccharacter_name'], 0, 1, 'UTF-8'), 'UTF-8')
: strtoupper(substr((string) $character['cl_sccharacter_name'], 0, 1));
?>
<section class="hero">
<?php if ($avatar !== ''): ?>
<img class="avatar" src="<?php echo htmlspecialchars($avatar, ENT_QUOTES, 'UTF-8'); ?>" alt="Avatar de <?php echo htmlspecialchars((string) $character['cl_sccharacter_name'], ENT_QUOTES, 'UTF-8'); ?>" loading="lazy" onerror="this.replaceWith(Object.assign(document.createElement('div'), {className:'avatar-fallback', textContent:'<?php echo htmlspecialchars($initial, ENT_QUOTES, 'UTF-8'); ?>'}));">
<?php else: ?>
<div class="avatar-fallback"><?php echo htmlspecialchars($initial, ENT_QUOTES, 'UTF-8'); ?></div>
<?php endif; ?>
<div>
<h1><?php echo htmlspecialchars((string) $character['cl_sccharacter_name'], ENT_QUOTES, 'UTF-8'); ?></h1>
<div class="identity-grid">
<div class="identity-item">
<span class="identity-label">Rôle / Classe</span>
<span class="identity-value"><?php echo htmlspecialchars(trim((string) $character['cl_sccharacter_role']) !== '' ? (string) $character['cl_sccharacter_role'] : '—', ENT_QUOTES, 'UTF-8'); ?></span>
</div>
<div class="identity-item">
<span class="identity-label">Tag</span>
<span class="identity-value"><?php echo htmlspecialchars($character_org_tag !== '' ? $character_org_tag : '—', ENT_QUOTES, 'UTF-8'); ?></span>
</div>
<div class="identity-item">
<span class="identity-label">Handle</span>
<span class="identity-value"><?php echo htmlspecialchars(trim((string) ($character['cl_sccharacter_player_handle'] ?? '')) !== '' ? (string) $character['cl_sccharacter_player_handle'] : '—', ENT_QUOTES, 'UTF-8'); ?></span>
</div>
</div>
<div class="meta">
<span class="tag muted">Créé par <?php echo htmlspecialchars((string) $character['cl_sccharacter_creator_name'], ENT_QUOTES, 'UTF-8'); ?></span>
<span class="tag muted"><?php echo count($character_items); ?> équipement(s)</span>
</div>
<p><?php echo nl2br(htmlspecialchars(trim((string) $character['cl_sccharacter_description']) !== '' ? (string) $character['cl_sccharacter_description'] : 'Aucune description publique fournie pour ce personnage.', ENT_QUOTES, 'UTF-8')); ?></p>
</div>
</section>
<?php if ($character_items === []): ?>
<div class="message">
<p class="muted">Ce personnage na pas encore déquipement attribué.</p>
</div>
<?php else: ?>
<div class="equipment-sections">
<?php foreach ($character_items_by_category as $category_key => $category_items): ?>
<section class="equipment-section">
<div class="equipment-section-head">
<h3 class="equipment-section-title"><?php echo htmlspecialchars(sccharacters_item_category_label((string) $category_key), ENT_QUOTES, 'UTF-8'); ?></h3>
</div>
<div class="grid">
<?php foreach ($category_items as $item_row): ?>
<?php
$is_custom = ($item_row['cl_sccharacteritem_source'] ?? '') === 'custom';
$name = $is_custom
? (string) ($item_row['cl_sccharacteritem_custom_name'] ?? 'Objet personnalisé indisponible')
: (string) ($item_row['cl_sccharacteritem_base_name'] ?? 'Objet indisponible');
$type = $is_custom
? (string) ($item_row['cl_sccharacteritem_custom_type'] ?? '')
: (string) ($item_row['cl_sccharacteritem_base_type'] ?? '');
$subtype = $is_custom
? (string) ($item_row['cl_sccharacteritem_custom_subtype'] ?? '')
: (string) ($item_row['cl_sccharacteritem_base_subtype'] ?? '');
$uuid = $is_custom
? (string) ($item_row['cl_sccharacteritem_custom_uuid'] ?? '')
: (string) ($item_row['cl_sccharacteritem_base_uuid'] ?? '');
$category = sccharacters_resolve_item_category(
(string) ($item_row['cl_sccharacteritem_slot'] ?? ''),
$type,
$subtype
);
$quantity = isset($item_row['cl_sccharacteritem_quantity']) && (int) $item_row['cl_sccharacteritem_quantity'] > 0
? (int) $item_row['cl_sccharacteritem_quantity']
: null;
$title = $quantity !== null ? $quantity . 'x ' . $name : $name;
$note = trim((string) ($item_row['cl_sccharacteritem_note'] ?? ''));
$stats = $is_custom ? ($custom_stats_by_itemcustom[(int) ($item_row['cl_sccharacteritem_scitemcustom_id'] ?? 0)] ?? []) : [];
$image_url = $uuid !== '' ? 'https://cstone.space/uifimages/' . rawurlencode($uuid) . '.png' : '';
?>
<article class="card">
<?php if ($image_url !== ''): ?>
<div class="preview-container">
<img class="thumb" src="<?php echo htmlspecialchars($image_url, ENT_QUOTES, 'UTF-8'); ?>" alt="Aperçu de <?php echo htmlspecialchars($name, ENT_QUOTES, 'UTF-8'); ?>" loading="lazy" onerror="this.replaceWith(Object.assign(document.createElement('div'), {className:'thumb-fallback', textContent:'◈'}));">
<div class="preview-floating" aria-hidden="true">
<img src="<?php echo htmlspecialchars($image_url, ENT_QUOTES, 'UTF-8'); ?>" alt="" loading="lazy" onerror="this.closest('.preview-floating').remove();">
</div>
</div>
<?php else: ?>
<div class="thumb-fallback"></div>
<?php endif; ?>
<div class="card-body">
<h3><?php echo htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); ?></h3>
<div class="tags">
<span class="tag <?php echo $is_custom ? 'tag-primary' : ''; ?>"><?php echo $is_custom ? 'Objet perso.' : 'Base dobjets'; ?></span>
<span class="tag"><?php echo htmlspecialchars(sccharacters_item_category_label($category), ENT_QUOTES, 'UTF-8'); ?></span>
<?php if ($type !== ''): ?><span class="tag muted"><?php echo htmlspecialchars($type, ENT_QUOTES, 'UTF-8'); ?></span><?php endif; ?>
<?php if ($subtype !== ''): ?><span class="tag muted"><?php echo htmlspecialchars($subtype, ENT_QUOTES, 'UTF-8'); ?></span><?php endif; ?>
</div>
<?php if ($stats !== []): ?>
<div class="stats">
<?php foreach ($stats as $stat_row): ?>
<?php
$sign = (string) ($stat_row['cl_scitemcustomstat_sign'] ?? '');
$value = rtrim(rtrim(number_format((float) ($stat_row['cl_scitemcustomstat_value'] ?? 0), 2, '.', ''), '0'), '.');
if ($value === '') {
$value = '0';
}
?>
<span class="stat"><?php echo htmlspecialchars((string) $stat_row['cl_scstatsitem_name'] . ' : ' . $sign . $value . ' ' . (string) $stat_row['cl_scstatsitem_unit'], ENT_QUOTES, 'UTF-8'); ?></span>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if ($note !== ''): ?>
<p class="muted"><?php echo nl2br(htmlspecialchars($note, ENT_QUOTES, 'UTF-8')); ?></p>
<?php endif; ?>
</div>
</article>
<?php endforeach; ?>
</div>
</section>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
</body>
</html>

3812
sccharacters.php Normal file

File diff suppressed because it is too large Load Diff

1907
scitemcustom.php Normal file

File diff suppressed because it is too large Load Diff

571
scitems.php Normal file
View File

@ -0,0 +1,571 @@
<?php
require_once __DIR__ . '/db/auth.php';
require_once __DIR__ . '/db/scitems.php';
auth_start_session();
auth_bootstrap();
auth_handle_page_access_post('scitems.php', "Base d'Objets");
auth_require_page_access('scitems.php', "Base d'Objets");
scitems_bootstrap();
$flash = auth_flash_get();
$flash_type = $flash['type'] ?? '';
$flash_message = $flash['message'] ?? '';
$db = db();
$csrf_token = auth_csrf_token();
// Pagination
$limit = 20;
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
if ($page < 1) $page = 1;
$offset = ($page - 1) * $limit;
// Search
$search = isset($_GET['search']) ? trim($_GET['search']) : '';
$where = "1=1";
$params = [];
if ($search !== '') {
$where = "(cl_scobjs_name LIKE :search OR cl_scobjs_type LIKE :search OR cl_scobjs_subtype LIKE :search OR cl_scobjs_uuid LIKE :search)";
$params['search'] = "%$search%";
}
// Handle POST actions
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$submitted_csrf = $_POST['csrf_token'] ?? '';
if (!auth_validate_csrf($submitted_csrf)) {
auth_flash_set('error', 'Jeton CSRF invalide.');
header('Location: scitems.php');
exit;
}
$action = $_POST['action'] ?? '';
if ($action === 'import_json') {
if (!isset($_FILES['json_file']) || $_FILES['json_file']['error'] !== UPLOAD_ERR_OK) {
auth_flash_set('error', 'Erreur lors du téléchargement du fichier JSON.');
} else {
$jsonData = file_get_contents($_FILES['json_file']['tmp_name']);
$items = json_decode($jsonData, true);
if (!is_array($items)) {
auth_flash_set('error', 'Format JSON invalide (doit être un tableau).');
} else {
$count_new = 0;
$count_updated = 0;
$stmt_check = $db->prepare("SELECT cl_scobjs_id FROM tbl_scobjs WHERE cl_scobjs_uuid = :uuid");
$stmt_insert = $db->prepare("INSERT INTO tbl_scobjs (cl_scobjs_name, cl_scobjs_type, cl_scobjs_subtype, cl_scobjs_uuid, cl_scobjs_rarity, cl_scobjs_about, cl_scobjs_description) VALUES (:name, :type, :subtype, :uuid, '', '', :description)");
$stmt_update = $db->prepare("UPDATE tbl_scobjs SET cl_scobjs_name = :name, cl_scobjs_type = :type, cl_scobjs_subtype = :subtype, cl_scobjs_description = :description WHERE cl_scobjs_uuid = :uuid");
foreach ($items as $item) {
$uuid = trim((string) ($item['reference'] ?? ($item['stdItem']['UUID'] ?? '')));
if ($uuid === '') continue;
$name = trim((string) ($item['Name'] ?? ($item['stdItem']['Name'] ?? '')));
$classification = trim((string) ($item['classification'] ?? ''));
$parts = $classification !== '' ? explode('.', $classification) : [];
$type = trim((string) ($parts[1] ?? ($item['type'] ?? '')));
$subtype = trim((string) ($parts[2] ?? ($item['subType'] ?? '')));
$description = trim((string) ($item['Description'] ?? ($item['stdItem']['Description'] ?? '')));
$payload = [
'name' => $name,
'type' => $type,
'subtype' => $subtype,
'uuid' => $uuid,
'description' => $description !== '' ? $description : null,
];
$stmt_check->execute(['uuid' => $uuid]);
if ($stmt_check->fetch()) {
$stmt_update->execute($payload);
$count_updated++;
} else {
$stmt_insert->execute($payload);
$count_new++;
}
}
auth_flash_set('success', "Importation terminée : $count_new nouveaux, $count_updated mis à jour.");
}
}
header('Location: scitems.php');
exit;
}
if ($action === 'update_item') {
$id = (int)$_POST['id'];
$rarity = trim($_POST['rarity'] ?? '');
$about = trim($_POST['about'] ?? '');
$stmt = $db->prepare("UPDATE tbl_scobjs SET cl_scobjs_rarity = :rarity, cl_scobjs_about = :about WHERE cl_scobjs_id = :id");
$stmt->execute(['rarity' => $rarity, 'about' => $about, 'id' => $id]);
auth_flash_set('success', "Objet mis à jour avec succès.");
header('Location: scitems.php?page=' . $page . '&search=' . urlencode($search));
exit;
}
}
// Fetch items
$sql_count = "SELECT COUNT(*) FROM tbl_scobjs WHERE $where";
$stmt_count = $db->prepare($sql_count);
$stmt_count->execute($params);
$total_items = (int)$stmt_count->fetchColumn();
$total_pages = ceil($total_items / $limit);
$sql_items = "SELECT * FROM tbl_scobjs WHERE $where ORDER BY cl_scobjs_name ASC LIMIT $limit OFFSET $offset";
$stmt_items = $db->prepare($sql_items);
$stmt_items->execute($params);
$items_list = $stmt_items->fetchAll();
$current_session_user = $_SESSION['user'] ?? '';
$edit_id = isset($_GET['edit']) ? (int)$_GET['edit'] : 0;
$edit_item = null;
if ($edit_id > 0) {
$stmt_edit = $db->prepare("SELECT * FROM tbl_scobjs WHERE cl_scobjs_id = :id");
$stmt_edit->execute(['id' => $edit_id]);
$edit_item = $stmt_edit->fetch();
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gestion des Objets | R.E.A.C.T. Admin</title>
<link rel="stylesheet" type="text/css" href="css/styles.css">
<link rel="stylesheet" type="text/css" href="css/default.css">
<style>
:root {
--primary: #a29b78;
--primary-glow: rgba(162, 155, 120, 0.4);
--bg-dark: #080a0f;
--card-bg: rgba(20, 24, 33, 0.85);
--border-glow: rgba(162, 155, 120, 0.25);
--danger: #ff4d4d;
--success: #00ff88;
/* Rarity Colors */
--rarity-L: #ff8000; /* Legendary - Orange */
--rarity-E: #a335ee; /* Epic - Purple */
--rarity-R: #0070dd; /* Rare - Blue */
--rarity-U: #1eff00; /* Uncommon - Green */
--rarity-C: #ffffff; /* Common - White */
}
@font-face {
font-family: 'Electrolize';
src: url('fonts/Electrolize-Regular.ttf') format('truetype');
}
body {
background: radial-gradient(circle at top right, #1a1f2e, var(--bg-dark));
background-attachment: fixed;
color: #e0e0e0;
font-family: 'Electrolize', sans-serif;
margin: 0;
overflow-x: hidden;
min-height: 100vh;
}
.admin-layout {
display: flex;
flex-direction: column;
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
animation: fadeIn 0.6s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.admin-topbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 2rem;
background: var(--card-bg);
backdrop-filter: blur(10px);
border: 1px solid var(--border-glow);
border-radius: 12px;
margin-bottom: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.topbar-info h1 {
margin: 0;
font-size: 1.5rem;
letter-spacing: 2px;
text-transform: uppercase;
background: linear-gradient(90deg, #fff, var(--primary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.topbar-info p {
margin: 0.25rem 0 0;
font-size: 0.85rem;
color: var(--primary);
opacity: 0.8;
}
.topbar-actions {
display: flex;
gap: 1rem;
flex-wrap: wrap;
align-items: center;
}
.session-user {
opacity: 0.85;
}
.topbar-actions {
display: flex;
gap: 1rem;
}
.btn-modern {
padding: 0.6rem 1.2rem;
border: 1px solid var(--primary);
background: transparent;
color: #fff;
font-family: 'Electrolize', sans-serif;
font-size: 0.9rem;
text-transform: uppercase;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 4px;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 5px;
}
.btn-modern:hover {
background: var(--primary);
color: var(--bg-dark);
box-shadow: 0 0 15px var(--primary-glow);
}
.btn-modern.danger { border-color: var(--danger); color: var(--danger); }
.btn-modern.danger:hover { background: var(--danger); color: #fff; }
.admin-grid {
display: grid;
grid-template-columns: 1fr 3fr;
gap: 2rem;
}
@media (max-width: 1024px) {
.admin-grid { grid-template-columns: 1fr; }
}
.glass-card {
background: var(--card-bg);
backdrop-filter: blur(12px);
border: 1px solid var(--border-glow);
border-radius: 12px;
padding: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
height: fit-content;
}
.glass-card h2 {
margin-top: 0;
margin-bottom: 1.5rem;
font-size: 1.25rem;
color: var(--primary);
border-bottom: 1px solid var(--border-glow);
padding-bottom: 0.75rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.form-group { margin-bottom: 1.5rem; }
.form-group label { display: block; margin-bottom: 0.5rem; font-size: 0.85rem; color: #aaa; text-transform: uppercase; }
.form-control {
width: 100%;
padding: 0.8rem 1rem;
background: rgba(0, 0, 0, 0.3);
border: 1px solid #444;
border-radius: 4px;
color: #fff;
font-family: 'Electrolize', sans-serif;
transition: border-color 0.3s;
}
.form-control:focus { outline: none; border-color: var(--primary); background: rgba(0, 0, 0, 0.5); }
select.form-control { background: #353b45; color: #fff; border-color: #565d68; color-scheme: dark; }
select.form-control:focus { background: #3d444f; color: #fff; }
select.form-control option { background: #353b45; color: #fff; }
select.form-control option:checked { background: #4a5260; color: #fff; }
.modern-table { width: 100%; border-collapse: separate; border-spacing: 0 8px; }
.modern-table th { text-align: left; padding: 1rem; font-size: 0.8rem; text-transform: uppercase; color: var(--primary); opacity: 0.7; }
.modern-table td { padding: 1rem; background: rgba(255, 255, 255, 0.03); border-top: 1px solid rgba(255, 255, 255, 0.05); border-bottom: 1px solid rgba(255, 255, 255, 0.05); vertical-align: top; }
.modern-table td:first-child { border-left: 1px solid rgba(255, 255, 255, 0.05); border-radius: 8px 0 0 8px; }
.modern-table td:last-child { border-right: 1px solid rgba(255, 255, 255, 0.05); border-radius: 0 8px 8px 0; }
.modern-table tr:hover td { background: rgba(162, 155, 120, 0.05); }
.flash { padding: 1rem 1.5rem; border-radius: 8px; margin-bottom: 1.5rem; font-size: 0.9rem; border-left: 4px solid var(--primary); background: rgba(162, 155, 120, 0.1); }
.flash.error { border-color: var(--danger); background: rgba(255, 77, 77, 0.1); color: #ffbaba; }
.flash.success { border-color: var(--success); background: rgba(0, 255, 136, 0.1); color: #baffda; }
.pagination { display: flex; gap: 0.5rem; margin-top: 2rem; justify-content: center; }
.page-link {
padding: 0.5rem 1rem;
border: 1px solid var(--border-glow);
background: var(--card-bg);
color: #fff;
text-decoration: none;
border-radius: 4px;
transition: all 0.2s;
}
.page-link:hover, .page-link.active { background: var(--primary); color: var(--bg-dark); }
.search-container { margin-bottom: 1.5rem; display: flex; gap: 10px; }
.search-container input { flex: 1; }
.badge { padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.7rem; text-transform: uppercase; background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); }
.item-name { color: var(--primary); font-weight: bold; display: block; font-size: 1rem; margin-bottom: 4px; }
.item-meta { font-size: 0.75rem; color: #888; display: block; }
.item-uuid { font-size: 0.75rem; color: #777; font-family: monospace; word-break: break-all; display: block; margin-bottom: 4px; }
.item-about-cell { font-size: 0.85rem; color: #ccc; line-height: 1.4; }
/* Preview System */
.preview-container {
position: relative;
width: 80px;
height: 80px;
cursor: zoom-in;
}
.item-preview {
width: 80px;
height: 80px;
object-fit: cover;
border-radius: 4px;
border: 1px solid var(--border-glow);
background: rgba(0,0,0,0.5);
display: block;
}
.preview-floating {
visibility: hidden;
opacity: 0;
position: absolute;
top: -10px;
left: 95px;
z-index: 1000;
padding: 5px;
background: var(--card-bg);
border: 1px solid var(--primary);
border-radius: 8px;
box-shadow: 0 10px 40px rgba(0,0,0,0.9), 0 0 20px var(--primary-glow);
backdrop-filter: blur(15px);
transition: opacity 0.3s ease, visibility 0.3s;
pointer-events: none;
}
.preview-floating img {
width: 350px;
height: 350px;
object-fit: contain;
display: block;
border-radius: 4px;
}
.preview-container:hover .preview-floating {
visibility: visible;
opacity: 1;
}
/* Rarity Classes */
.rarity-L { color: var(--rarity-L) !important; text-shadow: 0 0 10px rgba(255, 128, 0, 0.3); }
.rarity-E { color: var(--rarity-E) !important; text-shadow: 0 0 10px rgba(163, 53, 238, 0.3); }
.rarity-R { color: var(--rarity-R) !important; text-shadow: 0 0 10px rgba(0, 112, 221, 0.3); }
.rarity-U { color: var(--rarity-U) !important; text-shadow: 0 0 10px rgba(30, 255, 0, 0.3); }
.rarity-C { color: var(--rarity-C) !important; }
.nav-tabs { display: flex; gap: 1rem; margin-bottom: 2rem; border-bottom: 1px solid var(--border-glow); padding-bottom: 1rem; }
.nav-tabs a { text-decoration: none; color: #888; text-transform: uppercase; font-size: 0.9rem; transition: color 0.3s; }
.nav-tabs a:hover, .nav-tabs a.active { color: var(--primary); }
</style>
</head>
<body>
<?php echo auth_render_page_access_widget('scitems.php', "Base d'Objets"); ?>
<div class="admin-layout">
<header class="admin-topbar">
<div class="topbar-info">
<h1>R.E.A.C.T. Objects Control</h1>
<p>Niveau d'accès : <strong>Administrateur</strong></p>
</div>
<div class="topbar-actions">
<span class="session-user">Connecté : <strong><?php echo htmlspecialchars($current_session_user); ?></strong></span>
<a href="index.php" class="btn-modern">Retour au site</a>
<a href="logout.php" class="btn-modern danger">Déconnexion</a>
</div>
</header>
<?php echo auth_render_app_nav('scitems.php'); ?>
<?php if ($flash_message !== ''): ?>
<div class="flash <?php echo htmlspecialchars($flash_type); ?>">
<?php echo htmlspecialchars($flash_message); ?>
</div>
<?php endif; ?>
<div class="admin-grid">
<div class="side-panel">
<section class="glass-card" style="margin-bottom: 2rem;">
<h2>Importation JSON</h2>
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>">
<input type="hidden" name="action" value="import_json">
<div class="form-group">
<label for="json_file">Fichier JSON</label>
<input type="file" name="json_file" id="json_file" class="form-control" accept=".json" required>
</div>
<button type="submit" class="btn-modern" style="width: 100%;">Importer / Mettre à jour</button>
</form>
</section>
<?php if ($edit_item): ?>
<section class="glass-card">
<h2>Editer Objet</h2>
<form method="post">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>">
<input type="hidden" name="action" value="update_item">
<input type="hidden" name="id" value="<?php echo $edit_item['cl_scobjs_id']; ?>">
<div class="form-group">
<label>Nom</label>
<div class="form-control" style="background: rgba(255,255,255,0.05); border-color: transparent;">
<?php echo htmlspecialchars($edit_item['cl_scobjs_name']); ?>
</div>
</div>
<div class="form-group">
<label for="rarity">Rareté</label>
<select name="rarity" id="rarity" class="form-control">
<option value="" <?php echo $edit_item['cl_scobjs_rarity'] === '' ? 'selected' : ''; ?>>- Sélectionner -</option>
<option value="L" <?php echo $edit_item['cl_scobjs_rarity'] === 'L' ? 'selected' : ''; ?>>Legendary (L)</option>
<option value="E" <?php echo $edit_item['cl_scobjs_rarity'] === 'E' ? 'selected' : ''; ?>>Epic (E)</option>
<option value="R" <?php echo $edit_item['cl_scobjs_rarity'] === 'R' ? 'selected' : ''; ?>>Rare (R)</option>
<option value="U" <?php echo $edit_item['cl_scobjs_rarity'] === 'U' ? 'selected' : ''; ?>>Uncommon (U)</option>
<option value="C" <?php echo $edit_item['cl_scobjs_rarity'] === 'C' ? 'selected' : ''; ?>>Common (C)</option>
</select>
</div>
<div class="form-group">
<label for="about">Zone Admin / Infos</label>
<textarea name="about" id="about" class="form-control" rows="5" placeholder="Saisir les informations ici..."><?php echo htmlspecialchars($edit_item['cl_scobjs_about']); ?></textarea>
</div>
<div style="display: flex; gap: 10px;">
<button type="submit" class="btn-modern" style="flex: 2;">Sauvegarder</button>
<a href="scitems.php?page=<?php echo $page; ?>&search=<?php echo urlencode($search); ?>" class="btn-modern danger" style="flex: 1;">X</a>
</div>
</form>
</section>
<?php endif; ?>
</div>
<main class="main-panel">
<section class="glass-card">
<h2>
Base de Données d'Objets
<span style="font-size: 0.8rem; opacity: 0.6;"><?php echo $total_items; ?> entrées</span>
</h2>
<div class="search-container">
<form method="get" style="display: flex; width: 100%; gap: 10px;">
<input type="text" name="search" class="form-control" placeholder="Rechercher par nom, type, uuid..." value="<?php echo htmlspecialchars($search); ?>">
<button type="submit" class="btn-modern">Filtrer</button>
<?php if ($search !== ''): ?>
<a href="scitems.php" class="btn-modern danger">Reset</a>
<?php endif; ?>
</form>
</div>
<div style="overflow-x: auto;">
<table class="modern-table">
<thead>
<tr>
<th style="width: 80px;">Aperçu</th>
<th style="width: 35%;">Nom / UUID / Type</th>
<th>About</th>
<th style="text-align: right; width: 100px;">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($items_list)): ?>
<tr><td colspan="4" style="text-align: center; padding: 3rem; color: #666;">Aucun objet trouvé.</td></tr>
<?php else: ?>
<?php foreach ($items_list as $item): ?>
<?php
$rarityClass = '';
if ($item['cl_scobjs_rarity']) {
$rarityClass = 'rarity-' . $item['cl_scobjs_rarity'];
}
$imageUrl = "https://cstone.space/uifimages/" . $item['cl_scobjs_uuid'] . ".png";
?>
<tr>
<td>
<div class="preview-container">
<img src="<?php echo $imageUrl; ?>" class="item-preview" alt="" loading="lazy">
<div class="preview-floating">
<img src="<?php echo $imageUrl; ?>" alt="">
</div>
</div>
</td>
<td>
<span class="item-name <?php echo $rarityClass; ?>"><?php echo htmlspecialchars($item['cl_scobjs_name']); ?></span>
<span class="item-uuid"><?php echo htmlspecialchars($item['cl_scobjs_uuid']); ?></span>
<span class="item-meta">
<?php echo htmlspecialchars($item['cl_scobjs_type']); ?>
<?php if($item['cl_scobjs_subtype']) echo " / " . htmlspecialchars($item['cl_scobjs_subtype']); ?>
</span>
</td>
<td class="item-about-cell">
<?php echo nl2br(htmlspecialchars($item['cl_scobjs_about'])); ?>
</td>
<td style="text-align: right;">
<a href="scitems.php?edit=<?php echo $item['cl_scobjs_id']; ?>&page=<?php echo $page; ?>&search=<?php echo urlencode($search); ?>" class="btn-modern btn-mini">Editer</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<?php if ($total_pages > 1): ?>
<div class="pagination">
<?php for ($i = 1; $i <= $total_pages; $i++): ?>
<?php if ($i == 1 || $i == $total_pages || ($i >= $page - 2 && $i <= $page + 2)): ?>
<a href="scitems.php?page=<?php echo $i; ?>&search=<?php echo urlencode($search); ?>" class="page-link <?php echo $i == $page ? 'active' : ''; ?>">
<?php echo $i; ?>
</a>
<?php elseif ($i == $page - 3 || $i == $page + 3): ?>
<span style="padding: 0.5rem;">...</span>
<?php endif; ?>
<?php endfor; ?>
</div>
<?php endif; ?>
</section>
</main>
</div>
</div>
</body>
</html>

388
scmanufactures.php Normal file
View File

@ -0,0 +1,388 @@
<?php
require_once __DIR__ . '/db/auth.php';
auth_start_session();
auth_bootstrap();
auth_handle_page_access_post('scmanufactures.php', 'Manufactures');
auth_require_page_access('scmanufactures.php', 'Manufactures');
$flash = auth_flash_get();
$flash_type = $flash['type'] ?? '';
$flash_message = $flash['message'] ?? '';
$db = db();
$csrf_token = auth_csrf_token();
// Handle POST actions
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$submitted_csrf = $_POST['csrf_token'] ?? '';
if (!auth_validate_csrf($submitted_csrf)) {
auth_flash_set('error', 'Jeton CSRF invalide.');
header('Location: scmanufactures.php');
exit;
}
$action = $_POST['action'] ?? '';
// Add manufacture
if ($action === 'add_manufacture') {
$name = trim($_POST['name'] ?? '');
if ($name !== '') {
try {
$stmt = $db->prepare("INSERT INTO tbl_scmanufactures (cl_scmanufactures_name) VALUES (:name)");
$stmt->execute(['name' => $name]);
auth_flash_set('success', 'Manufacture ajoutée avec succès.');
} catch (PDOException $e) {
if ($e->getCode() == 23000) {
auth_flash_set('error', 'Cette manufacture existe déjà.');
} else {
auth_flash_set('error', 'Erreur lors de l\'ajout : ' . $e->getMessage());
}
}
} else {
auth_flash_set('error', 'Le nom de la manufacture est requis.');
}
header('Location: scmanufactures.php');
exit;
}
// Update manufacture
if ($action === 'update_manufacture') {
$id = (int)($_POST['manufacture_id'] ?? 0);
$name = trim($_POST['name'] ?? '');
if ($id > 0 && $name !== '') {
try {
$stmt = $db->prepare("UPDATE tbl_scmanufactures SET cl_scmanufactures_name = :name WHERE cl_scmanufactures_id = :id");
$stmt->execute(['name' => $name, 'id' => $id]);
auth_flash_set('success', 'Manufacture mise à jour.');
} catch (PDOException $e) {
auth_flash_set('error', 'Erreur lors de la mise à jour : ' . $e->getMessage());
}
} else {
auth_flash_set('error', 'Données invalides.');
}
header('Location: scmanufactures.php');
exit;
}
// Delete manufacture
if ($action === 'delete_manufacture') {
$id = (int)($_POST['manufacture_id'] ?? 0);
if ($id > 0) {
try {
$stmt = $db->prepare("DELETE FROM tbl_scmanufactures WHERE cl_scmanufactures_id = :id");
$stmt->execute(['id' => $id]);
auth_flash_set('success', 'Manufacture supprimée.');
} catch (PDOException $e) {
auth_flash_set('error', 'Erreur lors de la suppression. Assurez-vous qu\'aucun vaisseau n\'est lié à cette manufacture.');
}
}
header('Location: scmanufactures.php');
exit;
}
}
// Fetch all manufactures
$stmt_list = $db->query("SELECT * FROM tbl_scmanufactures ORDER BY cl_scmanufactures_name ASC");
$manufactures = $stmt_list->fetchAll();
$current_session_user = $_SESSION['user'] ?? '';
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Manufactures | R.E.A.C.T. Admin</title>
<link rel="stylesheet" type="text/css" href="css/styles.css">
<link rel="stylesheet" type="text/css" href="css/default.css">
<style>
:root {
--primary: #a29b78;
--primary-glow: rgba(162, 155, 120, 0.4);
--bg-dark: #080a0f;
--card-bg: rgba(20, 24, 33, 0.85);
--border-glow: rgba(162, 155, 120, 0.25);
--danger: #ff4d4d;
--success: #00ff88;
}
@font-face {
font-family: 'Electrolize';
src: url('fonts/Electrolize-Regular.ttf') format('truetype');
}
body {
background: radial-gradient(circle at top right, #1a1f2e, var(--bg-dark));
background-attachment: fixed;
color: #e0e0e0;
font-family: 'Electrolize', sans-serif;
margin: 0;
overflow-x: hidden;
min-height: 100vh;
}
.admin-layout {
display: flex;
flex-direction: column;
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
animation: fadeIn 0.6s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.admin-topbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 2rem;
background: var(--card-bg);
backdrop-filter: blur(10px);
border: 1px solid var(--border-glow);
border-radius: 12px;
margin-bottom: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.topbar-info h1 {
margin: 0;
font-size: 1.5rem;
letter-spacing: 2px;
text-transform: uppercase;
background: linear-gradient(90deg, #fff, var(--primary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.topbar-info p {
margin: 0.25rem 0 0;
font-size: 0.85rem;
color: var(--primary);
opacity: 0.8;
}
.topbar-actions {
display: flex;
gap: 1rem;
flex-wrap: wrap;
align-items: center;
}
.session-user {
opacity: 0.85;
}
.btn-modern {
padding: 0.6rem 1.2rem;
border: 1px solid var(--primary);
background: transparent;
color: #fff;
font-family: 'Electrolize', sans-serif;
font-size: 0.9rem;
text-transform: uppercase;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 4px;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 5px;
}
.btn-modern:hover {
background: var(--primary);
color: var(--bg-dark);
box-shadow: 0 0 15px var(--primary-glow);
}
.btn-modern.danger { border-color: var(--danger); color: var(--danger); }
.btn-modern.danger:hover { background: var(--danger); color: #fff; }
.btn-mini { padding: 0.3rem 0.6rem; font-size: 0.75rem; }
.nav-tabs { display: flex; gap: 1rem; margin-bottom: 2rem; border-bottom: 1px solid var(--border-glow); padding-bottom: 1rem; }
.nav-tabs a { text-decoration: none; color: #888; text-transform: uppercase; font-size: 0.9rem; transition: color 0.3s; }
.nav-tabs a:hover, .nav-tabs a.active { color: var(--primary); }
.admin-grid {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 2rem;
}
@media (max-width: 1024px) {
.admin-grid { grid-template-columns: 1fr; }
}
.glass-card {
background: var(--card-bg);
backdrop-filter: blur(12px);
border: 1px solid var(--border-glow);
border-radius: 12px;
padding: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
height: fit-content;
}
.glass-card h2 {
margin-top: 0;
margin-bottom: 1.5rem;
font-size: 1.25rem;
color: var(--primary);
border-bottom: 1px solid var(--border-glow);
padding-bottom: 0.75rem;
}
.form-group { margin-bottom: 1.5rem; }
.form-group label { display: block; margin-bottom: 0.5rem; font-size: 0.85rem; color: #aaa; text-transform: uppercase; }
.form-control {
width: 100%;
padding: 0.8rem 1rem;
background: rgba(0, 0, 0, 0.3);
border: 1px solid #444;
border-radius: 4px;
color: #fff;
font-family: 'Electrolize', sans-serif;
transition: border-color 0.3s;
}
.form-control:focus { outline: none; border-color: var(--primary); background: rgba(0, 0, 0, 0.5); }
.modern-table { width: 100%; border-collapse: separate; border-spacing: 0 8px; }
.modern-table th { text-align: left; padding: 1rem; font-size: 0.8rem; text-transform: uppercase; color: var(--primary); opacity: 0.7; }
.modern-table td { padding: 1rem; background: rgba(255, 255, 255, 0.03); border-top: 1px solid rgba(255, 255, 255, 0.05); border-bottom: 1px solid rgba(255, 255, 255, 0.05); }
.modern-table td:first-child { border-left: 1px solid rgba(255, 255, 255, 0.05); border-radius: 8px 0 0 8px; }
.modern-table td:last-child { border-right: 1px solid rgba(255, 255, 255, 0.05); border-radius: 0 8px 8px 0; }
.modern-table tr:hover td { background: rgba(162, 155, 120, 0.05); }
.flash { padding: 1rem 1.5rem; border-radius: 8px; margin-bottom: 1.5rem; font-size: 0.9rem; border-left: 4px solid var(--primary); background: rgba(162, 155, 120, 0.1); }
.flash.error { border-color: var(--danger); background: rgba(255, 77, 77, 0.1); color: #ffbaba; }
.flash.success { border-color: var(--success); background: rgba(0, 255, 136, 0.1); color: #baffda; }
</style>
</head>
<body>
<?php echo auth_render_page_access_widget('scmanufactures.php', 'Manufactures'); ?>
<div class="admin-layout">
<header class="admin-topbar">
<div class="topbar-info">
<h1>Gestion Manufactures</h1>
<p>Niveau d\'accès : <strong>Administrateur</strong></p>
</div>
<div class="topbar-actions">
<span class="session-user">Connecté : <strong><?php echo htmlspecialchars($current_session_user); ?></strong></span>
<a href="index.php" class="btn-modern">Retour au site</a>
<a href="logout.php" class="btn-modern danger">Déconnexion</a>
</div>
</header>
<?php echo auth_render_app_nav('scmanufactures.php'); ?>
<?php if ($flash_message !== ''): ?>
<div class="flash <?php echo htmlspecialchars($flash_type); ?>">
<?php echo htmlspecialchars($flash_message); ?>
</div>
<?php endif; ?>
<div class="admin-grid">
<!-- Left Column: Add/Edit -->
<div class="side-panel">
<section class="glass-card">
<h2 id="formTitle">Nouvelle Manufacture</h2>
<form id="manufactureForm" method="post">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>">
<input type="hidden" name="action" id="formAction" value="add_manufacture">
<input type="hidden" name="manufacture_id" id="manufactureId" value="">
<div class="form-group">
<label>Nom de la Manufacture</label>
<input type="text" name="name" id="manufactureName" class="form-control" required placeholder="ex: Roberts Space Industries">
</div>
<button type="submit" id="submitBtn" class="btn-modern" style="width: 100%;">Ajouter</button>
<button type="button" id="cancelBtn" class="btn-modern" style="width: 100%; margin-top: 10px; display: none;" onclick="resetForm()">Annuler</button>
</form>
</section>
</div>
<!-- Right Column: List -->
<main class="main-panel">
<section class="glass-card">
<h2>Liste des Manufactures</h2>
<div style="overflow-x: auto;">
<table class="modern-table">
<thead>
<tr>
<th>ID</th>
<th>Nom</th>
<th style="text-align: right;">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($manufactures)): ?>
<tr><td colspan="3" style="text-align: center; padding: 3rem; color: #666;">Aucune manufacture enregistrée.</td></tr>
<?php else: ?>
<?php foreach ($manufactures as $m): ?>
<tr>
<td style="width: 50px; opacity: 0.5;">#<?php echo $m['cl_scmanufactures_id']; ?></td>
<td>
<strong style="color: var(--primary); text-transform: uppercase;"><?php echo htmlspecialchars($m['cl_scmanufactures_name']); ?></strong>
</td>
<td style="text-align: right;">
<div style="display: flex; gap: 5px; justify-content: flex-end;">
<button type="button" class="btn-modern btn-mini"
onclick='editManufacture(<?php echo json_encode([
"id" => $m["cl_scmanufactures_id"],
"name" => $m["cl_scmanufactures_name"]
]); ?>)'>
Edit
</button>
<form method="post" onsubmit="return confirm('Supprimer cette manufacture ?');">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>">
<input type="hidden" name="action" value="delete_manufacture">
<input type="hidden" name="manufacture_id" value="<?php echo $m['cl_scmanufactures_id']; ?>">
<button type="submit" class="btn-modern btn-mini danger">X</button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</section>
</main>
</div>
</div>
<script>
function editManufacture(data) {
document.getElementById('formAction').value = 'update_manufacture';
document.getElementById('manufactureId').value = data.id;
document.getElementById('manufactureName').value = data.name;
document.getElementById('submitBtn').innerText = 'Mettre à jour';
document.getElementById('cancelBtn').style.display = 'block';
document.getElementById('formTitle').innerText = 'Modifier Manufacture';
document.getElementById('manufactureForm').scrollIntoView({ behavior: 'smooth' });
}
function resetForm() {
document.getElementById('formAction').value = 'add_manufacture';
document.getElementById('manufactureId').value = '';
document.getElementById('manufactureForm').reset();
document.getElementById('submitBtn').innerText = 'Ajouter';
document.getElementById('cancelBtn').style.display = 'none';
document.getElementById('formTitle').innerText = 'Nouvelle Manufacture';
}
</script>
</body>
</html>

1992
scmanutention.php Normal file

File diff suppressed because it is too large Load Diff

322
scmanutentionpublic.php Normal file
View File

@ -0,0 +1,322 @@
<?php
require_once __DIR__ . '/db/auth.php';
require_once __DIR__ . '/db/scstatsitem.php';
require_once __DIR__ . '/db/scitemcustom.php';
require_once __DIR__ . '/db/scmanutention.php';
auth_start_session();
auth_bootstrap();
scstatsitem_bootstrap();
scitemcustom_bootstrap();
scmanutention_bootstrap();
function scmanutentionpublic_item_name(array $item_row): string
{
return (string) ((($item_row['cl_scmanutentionitem_source'] ?? '') === 'custom')
? ($item_row['cl_scmanutentionitem_custom_name'] ?? '')
: ($item_row['cl_scmanutentionitem_base_name'] ?? ''));
}
function scmanutentionpublic_item_type(array $item_row): string
{
return (string) ((($item_row['cl_scmanutentionitem_source'] ?? '') === 'custom')
? ($item_row['cl_scmanutentionitem_custom_type'] ?? '')
: ($item_row['cl_scmanutentionitem_base_type'] ?? ''));
}
function scmanutentionpublic_item_subtype(array $item_row): string
{
return (string) ((($item_row['cl_scmanutentionitem_source'] ?? '') === 'custom')
? ($item_row['cl_scmanutentionitem_custom_subtype'] ?? '')
: ($item_row['cl_scmanutentionitem_base_subtype'] ?? ''));
}
function scmanutentionpublic_item_uuid(array $item_row): string
{
return (string) ((($item_row['cl_scmanutentionitem_source'] ?? '') === 'custom')
? ($item_row['cl_scmanutentionitem_custom_uuid'] ?? '')
: ($item_row['cl_scmanutentionitem_base_uuid'] ?? ''));
}
function scmanutentionpublic_item_rarity(array $item_row): string
{
return (string) ((($item_row['cl_scmanutentionitem_source'] ?? '') === 'custom')
? ($item_row['cl_scmanutentionitem_custom_rarity'] ?? '')
: ($item_row['cl_scmanutentionitem_base_rarity'] ?? ''));
}
function scmanutentionpublic_rarity_class(string $rarity): string
{
$rarity = strtoupper(trim($rarity));
return in_array($rarity, ['L', 'E', 'R', 'U', 'C'], true) ? 'rarity-' . $rarity : '';
}
function scmanutentionpublic_stat_preview(array $stat_row): string
{
$sign = (string) ($stat_row['cl_scitemcustomstat_sign'] ?? '');
$prefix = $sign === '-' ? '-' : ($sign === '+' ? '+' : '');
$value = number_format((float) ($stat_row['cl_scitemcustomstat_value'] ?? 0), 2, '.', '');
$value = rtrim(rtrim($value, '0'), '.');
if ($value === '') {
$value = '0';
}
$unit = trim((string) ($stat_row['cl_scstatsitem_unit'] ?? ''));
return trim((string) ($stat_row['cl_scstatsitem_name'] ?? '') . ' : ' . $prefix . $value . ($unit !== '' ? ' ' . $unit : ''));
}
$db = db();
$share_token = trim((string) ($_GET['share'] ?? ''));
$sheet = scmanutention_find_public_sheet_by_token($db, $share_token);
$items = $sheet ? scmanutention_fetch_items($db, (int) $sheet['cl_scmanutention_id']) : [];
$custom_stats_map = scmanutention_fetch_custom_stats_map($db, $items);
if (!$sheet) {
http_response_code(404);
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo htmlspecialchars($sheet ? ((string) $sheet['cl_scmanutention_title'] . ' | Fiche manutention') : 'Fiche introuvable', ENT_QUOTES, 'UTF-8'); ?></title>
<style>
:root {
--primary: #a29b78;
--primary-soft: rgba(162, 155, 120, 0.18);
--primary-border: rgba(162, 155, 120, 0.34);
--bg: #07090d;
--surface: rgba(15, 19, 28, 0.88);
--surface-soft: rgba(255,255,255,0.05);
--surface-border: rgba(255,255,255,0.08);
--text-main: #f2efe4;
--text-soft: rgba(242, 239, 228, 0.72);
--rarity-L: #ff8000;
--rarity-E: #a335ee;
--rarity-R: #0070dd;
--rarity-U: #1eff00;
--rarity-C: #ffffff;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background:
linear-gradient(rgba(6, 8, 12, 0.8), rgba(6, 8, 12, 0.92)),
url('https://robertsspaceindustries.com/media/1vllgn95062syr/background_blur/REACT-Background.jpg') center/cover fixed;
color: var(--text-main);
font-family: Arial, Helvetica, sans-serif;
}
.page {
width: min(1180px, calc(100vw - 2rem));
margin: 0 auto;
padding: 2rem 0 3rem;
}
.hero,
.panel,
.item-card,
.message {
background: var(--surface);
border: 1px solid var(--primary-border);
border-radius: 22px;
box-shadow: 0 22px 46px rgba(0,0,0,0.32);
backdrop-filter: blur(12px);
}
.hero,
.panel,
.message {
padding: 1.2rem;
margin-bottom: 1rem;
}
h1, h2 { margin-top: 0; }
p { line-height: 1.6; }
.meta,
.item-meta,
.stats-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
}
.badge {
display: inline-flex;
align-items: center;
padding: 0.34rem 0.62rem;
border-radius: 999px;
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.1);
color: var(--text-soft);
font-size: 0.82rem;
}
.items-list {
display: grid;
gap: 0.9rem;
}
.item-card {
padding: 0.95rem;
display: grid;
gap: 0.7rem;
}
.item-head {
display: flex;
gap: 0.85rem;
align-items: start;
}
.item-title-line {
display: flex;
align-items: baseline;
gap: 0.45rem;
flex-wrap: wrap;
}
.item-quantity-prefix {
font-weight: 800;
color: #fff2ca;
letter-spacing: 0.02em;
white-space: nowrap;
}
.item-preview {
width: 56px;
height: 56px;
object-fit: cover;
border-radius: 14px;
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.08);
flex: none;
}
.item-text { min-width: 0; display: grid; gap: 5px; }
.item-name { font-size: 1.05rem; font-weight: 700; line-height: 1.2; }
.helper, .muted { color: var(--text-soft); }
.item-extra {
padding: 0.72rem 0.85rem;
border-radius: 14px;
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.08);
}
.stat-pill {
display: inline-flex;
align-items: center;
min-height: 30px;
padding: 0.38rem 0.72rem;
border-radius: 12px;
background: linear-gradient(135deg, rgba(26, 74, 42, 0.9), rgba(18, 46, 28, 0.82));
border: 1px solid rgba(90, 255, 150, 0.85);
box-shadow: inset 0 0 0 1px rgba(140, 255, 188, 0.18), 0 0 0 1px rgba(34, 110, 58, 0.28), 0 10px 22px rgba(0,0,0,0.18);
font-size: 0.8rem;
font-weight: 700;
color: #dcffe9;
}
.rarity-L { color: var(--rarity-L); text-shadow: 0 0 12px rgba(255, 128, 0, 0.28); }
.rarity-E { color: var(--rarity-E); text-shadow: 0 0 12px rgba(163, 53, 238, 0.28); }
.rarity-R { color: var(--rarity-R); text-shadow: 0 0 12px rgba(0, 112, 221, 0.28); }
.rarity-U { color: var(--rarity-U); text-shadow: 0 0 12px rgba(30, 255, 0, 0.28); }
.rarity-C { color: var(--rarity-C); }
@media (max-width: 700px) {
.page { width: min(100%, calc(100vw - 1rem)); }
.item-head { flex-wrap: wrap; }
}
</style>
</head>
<body>
<div class="page">
<?php if (!$sheet): ?>
<section class="message">
<h1>Fiche introuvable</h1>
<p>Cette fiche nexiste pas, nest plus publique, ou le lien partagé nest plus valide.</p>
</section>
<?php else: ?>
<section class="hero">
<h1><?php echo htmlspecialchars((string) $sheet['cl_scmanutention_title'], ENT_QUOTES, 'UTF-8'); ?></h1>
<div class="meta">
<?php if (!empty($sheet['cl_scmanutention_type'])): ?>
<span class="badge"><?php echo htmlspecialchars((string) $sheet['cl_scmanutention_type'], ENT_QUOTES, 'UTF-8'); ?></span>
<?php endif; ?>
<?php if (!empty($sheet['cl_scmanutention_subtype'])): ?>
<span class="badge"><?php echo htmlspecialchars((string) $sheet['cl_scmanutention_subtype'], ENT_QUOTES, 'UTF-8'); ?></span>
<?php endif; ?>
<span class="badge"><?php echo count($items); ?> objet(s)</span>
<span class="badge">Publié par <?php echo htmlspecialchars((string) ($sheet['cl_scmanutention_owner_name'] ?? 'Inconnu'), ENT_QUOTES, 'UTF-8'); ?></span>
</div>
<?php if (!empty($sheet['cl_scmanutention_description'])): ?>
<p><?php echo nl2br(htmlspecialchars((string) $sheet['cl_scmanutention_description'], ENT_QUOTES, 'UTF-8')); ?></p>
<?php else: ?>
<p class="helper">Cette fiche publique ne contient pas encore de description.</p>
<?php endif; ?>
</section>
<section class="panel">
<h2>Contenu de la fiche</h2>
<?php if (empty($items)): ?>
<p class="muted">Aucun objet nest actuellement listé sur cette fiche.</p>
<?php else: ?>
<div class="items-list">
<?php foreach ($items as $item_row): ?>
<?php
$item_name = scmanutentionpublic_item_name($item_row);
$item_type = scmanutentionpublic_item_type($item_row);
$item_subtype = scmanutentionpublic_item_subtype($item_row);
$item_uuid = scmanutentionpublic_item_uuid($item_row);
$item_rarity_class = scmanutentionpublic_rarity_class(scmanutentionpublic_item_rarity($item_row));
$item_source = (string) ($item_row['cl_scmanutentionitem_source'] ?? 'base');
$item_custom_id = (int) ($item_row['cl_scmanutentionitem_scitemcustom_id'] ?? 0);
$item_stats = $item_source === 'custom' ? ($custom_stats_map[$item_custom_id] ?? []) : [];
?>
<article class="item-card">
<div class="item-head">
<?php if ($item_uuid !== ''): ?>
<img src="https://cstone.space/uifimages/<?php echo htmlspecialchars($item_uuid, ENT_QUOTES, 'UTF-8'); ?>.png" alt="" class="item-preview">
<?php endif; ?>
<div class="item-text">
<div class="item-title-line">
<span class="item-quantity-prefix"><?php echo (int) ($item_row['cl_scmanutentionitem_quantity'] ?? 1); ?>x</span>
<div class="item-name <?php echo htmlspecialchars($item_rarity_class, ENT_QUOTES, 'UTF-8'); ?>"><?php echo htmlspecialchars($item_name, ENT_QUOTES, 'UTF-8'); ?></div>
</div>
<div class="item-meta">
<span class="badge"><?php echo htmlspecialchars($item_type !== '' ? $item_type : 'Sans type', ENT_QUOTES, 'UTF-8'); ?></span>
<?php if ($item_subtype !== ''): ?>
<span class="badge"><?php echo htmlspecialchars($item_subtype, ENT_QUOTES, 'UTF-8'); ?></span>
<?php endif; ?>
<span class="badge"><?php echo $item_source === 'custom' ? 'Objet perso' : 'Base d\'objets'; ?></span>
<?php if (!empty($item_stats)): ?>
<?php foreach ($item_stats as $stat_row): ?>
<span class="stat-pill"><?php echo htmlspecialchars(scmanutentionpublic_stat_preview($stat_row), ENT_QUOTES, 'UTF-8'); ?></span>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</div>
<?php if (!empty($item_row['cl_scmanutentionitem_extra_info'])): ?>
<div class="item-extra"><?php echo nl2br(htmlspecialchars((string) $item_row['cl_scmanutentionitem_extra_info'], ENT_QUOTES, 'UTF-8')); ?></div>
<?php endif; ?>
</article>
<?php endforeach; ?>
</div>
<?php endif; ?>
</section>
<?php endif; ?>
</div>
</body>
</html>

642
scmining.php Normal file
View File

@ -0,0 +1,642 @@
<?php
require_once __DIR__ . '/db/auth.php';
auth_start_session();
auth_bootstrap();
auth_handle_page_access_post('scmining.php', 'Scanner Minage');
auth_require_page_access('scmining.php', 'Scanner Minage');
$flash = auth_flash_get();
$flash_type = $flash['type'] ?? '';
$flash_message = $flash['message'] ?? '';
$db = db();
$csrf_token = auth_csrf_token();
// Handle POST actions
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$submitted_csrf = $_POST['csrf_token'] ?? '';
if (!auth_validate_csrf($submitted_csrf)) {
auth_flash_set('error', 'Jeton CSRF invalide.');
header('Location: scmining.php');
exit;
}
$action = $_POST['action'] ?? '';
// Add mineral to list
if ($action === 'add_mineral') {
$obj_id = (int)$_POST['obj_id'];
$return_search = trim($_POST['return_search'] ?? '');
$return_page = max(1, (int)($_POST['return_page'] ?? 1));
if ($obj_id > 0) {
try {
$stmt = $db->prepare("INSERT INTO tbl_scmining (cl_scmining_obj_id, cl_scmining_scan_value, cl_scmining_max_occurrence) VALUES (:obj_id, 0, 1)");
$stmt->execute(['obj_id' => $obj_id]);
auth_flash_set('success', 'Minéral ajouté avec succès.');
} catch (PDOException $e) {
if ($e->getCode() == 23000) {
auth_flash_set('error', 'Ce minéral est déjà dans la liste.');
} else {
auth_flash_set('error', "Erreur lors de l'ajout : " . $e->getMessage());
}
}
}
header('Location: ' . scmining_search_url($return_search, $return_page));
exit;
}
// Update mineral values
if ($action === 'update_mineral') {
$mining_id = (int)$_POST['mining_id'];
$scan_value = (int)$_POST['scan_value'];
$max_occurrence = (int)$_POST['max_occurrence'];
$can_manual = isset($_POST['can_manual']) ? 1 : 0;
$can_land = isset($_POST['can_land']) ? 1 : 0;
$can_space = isset($_POST['can_space']) ? 1 : 0;
$stmt = $db->prepare("UPDATE tbl_scmining SET cl_scmining_scan_value = :scan, cl_scmining_max_occurrence = :occ, cl_scmining_can_manual = :manual, cl_scmining_can_land = :land, cl_scmining_can_space = :space WHERE cl_scmining_id = :id");
$stmt->execute([
'scan' => $scan_value,
'occ' => $max_occurrence,
'manual' => $can_manual,
'land' => $can_land,
'space' => $can_space,
'id' => $mining_id
]);
auth_flash_set('success', 'Valeurs mises à jour.');
header('Location: scmining.php');
exit;
}
// Remove mineral
if ($action === 'delete_mineral') {
$mining_id = (int)$_POST['mining_id'];
$stmt = $db->prepare("DELETE FROM tbl_scmining WHERE cl_scmining_id = :id");
$stmt->execute(['id' => $mining_id]);
auth_flash_set('success', 'Minéral retiré de la liste.');
header('Location: scmining.php');
exit;
}
}
// Search for adding items
function sc_normalize_rarity(?string $rarity): string
{
return strtoupper(trim((string) $rarity));
}
function sc_rarity_class(?string $rarity): string
{
$rarity = sc_normalize_rarity($rarity);
return in_array($rarity, ['L', 'E', 'R', 'U', 'C'], true) ? 'rarity-' . $rarity : 'rarity-none';
}
function sc_rarity_style(?string $rarity): string
{
return match (sc_normalize_rarity($rarity)) {
'L' => 'color:#ff8000 !important;text-shadow:0 0 10px rgba(255,128,0,0.3);',
'E' => 'color:#a335ee !important;text-shadow:0 0 10px rgba(163,53,238,0.3);',
'R' => 'color:#0070dd !important;text-shadow:0 0 10px rgba(0,112,221,0.3);',
'U' => 'color:#1eff00 !important;text-shadow:0 0 10px rgba(30,255,0,0.3);',
'C' => 'color:#ffffff !important;',
default => 'color:#8f96a3 !important;',
};
}
function scmining_search_url(string $search = '', int $page = 1): string
{
$params = [];
if ($search !== '') {
$params['search'] = $search;
}
if ($page > 1) {
$params['search_page'] = $page;
}
return 'scmining.php' . ($params ? '?' . http_build_query($params) : '');
}
$search = isset($_GET['search']) ? trim($_GET['search']) : '';
$search_page = max(1, (int)($_GET['search_page'] ?? 1));
$search_per_page = 10;
$search_results = [];
$search_total_results = 0;
$search_total_pages = 0;
$configured_matches = [];
if ($search !== '') {
$stmt_configured = $db->prepare("SELECT o.* FROM tbl_scobjs o JOIN tbl_scmining m ON m.cl_scmining_obj_id = o.cl_scobjs_id WHERE o.cl_scobjs_name LIKE :search ORDER BY o.cl_scobjs_name ASC, o.cl_scobjs_id ASC");
$stmt_configured->execute(['search' => "%$search%"]);
$configured_matches = $stmt_configured->fetchAll();
$search_where = "FROM tbl_scobjs o WHERE o.cl_scobjs_name LIKE :search AND o.cl_scobjs_id NOT IN (SELECT cl_scmining_obj_id FROM tbl_scmining)";
$stmt_search_count = $db->prepare("SELECT COUNT(*) " . $search_where);
$stmt_search_count->execute(['search' => "%$search%"]);
$search_total_results = (int)$stmt_search_count->fetchColumn();
$search_total_pages = max(1, (int)ceil($search_total_results / $search_per_page));
$search_page = min($search_page, $search_total_pages);
$search_offset = ($search_page - 1) * $search_per_page;
$stmt_search = $db->prepare("SELECT o.* " . $search_where . " ORDER BY o.cl_scobjs_name ASC, o.cl_scobjs_id ASC LIMIT :limit OFFSET :offset");
$stmt_search->bindValue(':search', "%$search%", PDO::PARAM_STR);
$stmt_search->bindValue(':limit', $search_per_page, PDO::PARAM_INT);
$stmt_search->bindValue(':offset', $search_offset, PDO::PARAM_INT);
$stmt_search->execute();
$search_results = $stmt_search->fetchAll();
}
// Fetch current mining list
$sql_list = "SELECT m.*, o.cl_scobjs_name, o.cl_scobjs_uuid, o.cl_scobjs_type, o.cl_scobjs_subtype, o.cl_scobjs_rarity
FROM tbl_scmining m
JOIN tbl_scobjs o ON m.cl_scmining_obj_id = o.cl_scobjs_id
ORDER BY o.cl_scobjs_name ASC";
$stmt_list = $db->query($sql_list);
$mining_list = $stmt_list->fetchAll();
$current_session_user = $_SESSION['user'] ?? '';
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Scanner Minage | R.E.A.C.T. Admin</title>
<link rel="stylesheet" type="text/css" href="css/styles.css">
<link rel="stylesheet" type="text/css" href="css/default.css">
<style>
:root {
--primary: #a29b78;
--primary-glow: rgba(162, 155, 120, 0.4);
--bg-dark: #080a0f;
--card-bg: rgba(20, 24, 33, 0.85);
--border-glow: rgba(162, 155, 120, 0.25);
--danger: #ff4d4d;
--success: #00ff88;
--rarity-L: #ff8000;
--rarity-E: #a335ee;
--rarity-R: #0070dd;
--rarity-U: #1eff00;
--rarity-C: #ffffff;
}
@font-face {
font-family: 'Electrolize';
src: url('fonts/Electrolize-Regular.ttf') format('truetype');
}
body {
background: radial-gradient(circle at top right, #1a1f2e, var(--bg-dark));
background-attachment: fixed;
color: #e0e0e0;
font-family: 'Electrolize', sans-serif;
margin: 0;
overflow-x: hidden;
min-height: 100vh;
}
.admin-layout {
display: flex;
flex-direction: column;
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
animation: fadeIn 0.6s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.admin-topbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 2rem;
background: var(--card-bg);
backdrop-filter: blur(10px);
border: 1px solid var(--border-glow);
border-radius: 12px;
margin-bottom: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.topbar-info h1 {
margin: 0;
font-size: 1.5rem;
letter-spacing: 2px;
text-transform: uppercase;
background: linear-gradient(90deg, #fff, var(--primary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.topbar-info p {
margin: 0.25rem 0 0;
font-size: 0.85rem;
color: var(--primary);
opacity: 0.8;
}
.topbar-actions {
display: flex;
gap: 1rem;
flex-wrap: wrap;
align-items: center;
}
.session-user {
opacity: 0.85;
}
.btn-modern {
padding: 0.6rem 1.2rem;
border: 1px solid var(--primary);
background: transparent;
color: #fff;
font-family: 'Electrolize', sans-serif;
font-size: 0.9rem;
text-transform: uppercase;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 4px;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 5px;
}
.btn-modern:hover {
background: var(--primary);
color: var(--bg-dark);
box-shadow: 0 0 15px var(--primary-glow);
}
.btn-modern.active {
background: var(--primary);
color: var(--bg-dark);
box-shadow: 0 0 15px var(--primary-glow);
}
.btn-modern.danger { border-color: var(--danger); color: var(--danger); }
.btn-modern.danger:hover { background: var(--danger); color: #fff; }
.btn-mini { padding: 0.3rem 0.6rem; font-size: 0.75rem; }
.nav-tabs { display: flex; gap: 1rem; margin-bottom: 2rem; border-bottom: 1px solid var(--border-glow); padding-bottom: 1rem; }
.nav-tabs a { text-decoration: none; color: #888; text-transform: uppercase; font-size: 0.9rem; transition: color 0.3s; }
.nav-tabs a:hover, .nav-tabs a.active { color: var(--primary); }
.admin-grid {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 2rem;
}
@media (max-width: 1024px) {
.admin-grid { grid-template-columns: 1fr; }
}
.glass-card {
background: var(--card-bg);
backdrop-filter: blur(12px);
border: 1px solid var(--border-glow);
border-radius: 12px;
padding: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
height: fit-content;
}
.glass-card h2 {
margin-top: 0;
margin-bottom: 1.5rem;
font-size: 1.25rem;
color: var(--primary);
border-bottom: 1px solid var(--border-glow);
padding-bottom: 0.75rem;
}
.form-group { margin-bottom: 1.5rem; }
.form-group label { display: block; margin-bottom: 0.5rem; font-size: 0.85rem; color: #aaa; text-transform: uppercase; }
.form-control {
width: 100%;
padding: 0.8rem 1rem;
background: rgba(0, 0, 0, 0.3);
border: 1px solid #444;
border-radius: 4px;
color: #fff;
font-family: 'Electrolize', sans-serif;
transition: border-color 0.3s;
}
.form-control:focus { outline: none; border-color: var(--primary); background: rgba(0, 0, 0, 0.5); }
.modern-table { width: 100%; border-collapse: separate; border-spacing: 0 8px; }
.modern-table th { text-align: left; padding: 1rem; font-size: 0.8rem; text-transform: uppercase; color: var(--primary); opacity: 0.7; }
.modern-table td { padding: 1rem; background: rgba(255, 255, 255, 0.03); border-top: 1px solid rgba(255, 255, 255, 0.05); border-bottom: 1px solid rgba(255, 255, 255, 0.05); }
.modern-table td:first-child { border-left: 1px solid rgba(255, 255, 255, 0.05); border-radius: 8px 0 0 8px; }
.modern-table td:last-child { border-right: 1px solid rgba(255, 255, 255, 0.05); border-radius: 0 8px 8px 0; }
.modern-table tr:hover td { background: rgba(162, 155, 120, 0.05); }
.flash { padding: 1rem 1.5rem; border-radius: 8px; margin-bottom: 1.5rem; font-size: 0.9rem; border-left: 4px solid var(--primary); background: rgba(162, 155, 120, 0.1); }
.flash.error { border-color: var(--danger); background: rgba(255, 77, 77, 0.1); color: #ffbaba; }
.flash.success { border-color: var(--success); background: rgba(0, 255, 136, 0.1); color: #baffda; }
.item-preview {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 4px;
border: 1px solid var(--border-glow);
background: rgba(0,0,0,0.5);
}
.search-result-item {
display: flex;
align-items: center;
gap: 15px;
padding: 10px;
background: rgba(255,255,255,0.05);
margin-bottom: 10px;
border-radius: 8px;
border: 1px solid rgba(255,255,255,0.1);
}
.search-result-info { flex: 1; }
.search-result-name { display: block; color: var(--primary); font-weight: bold; }
.search-result-uuid { display: block; font-size: 0.75rem; color: #777; font-family: monospace; word-break: break-all; margin-top: 2px; }
.search-result-meta { display: block; font-size: 0.75rem; color: #888; }
.search-results-section { margin-bottom: 1.25rem; }
.search-results-caption {
margin: 0 0 0.75rem;
font-size: 0.78rem;
color: #9a9a9a;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.search-result-item.is-configured {
border-color: rgba(162, 155, 120, 0.35);
background: rgba(162, 155, 120, 0.08);
}
.search-result-badge {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.35rem 0.65rem;
border-radius: 999px;
border: 1px solid rgba(162, 155, 120, 0.35);
color: var(--primary);
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.04em;
white-space: nowrap;
}
.search-results-summary {
margin-bottom: 1rem;
font-size: 0.8rem;
color: #9a9a9a;
}
.pagination-controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin-top: 1rem;
}
.pagination-controls .btn-modern {
min-width: 42px;
}
.pagination-status {
font-size: 0.8rem;
color: #9a9a9a;
margin-left: auto;
}
.rarity-L { color: var(--rarity-L) !important; text-shadow: 0 0 10px rgba(255, 128, 0, 0.3); }
.rarity-E { color: var(--rarity-E) !important; text-shadow: 0 0 10px rgba(163, 53, 238, 0.3); }
.rarity-R { color: var(--rarity-R) !important; text-shadow: 0 0 10px rgba(0, 112, 221, 0.3); }
.rarity-U { color: var(--rarity-U) !important; text-shadow: 0 0 10px rgba(30, 255, 0, 0.3); }
.rarity-C { color: var(--rarity-C) !important; }
.rarity-none { color: #8f96a3 !important; }
.val-input { width: 100px; text-align: center; }
.recovery-options {
display: flex;
flex-direction: column;
gap: 2px;
font-size: 0.75rem;
text-align: left;
}
.recovery-options label {
display: flex;
align-items: center;
gap: 5px;
cursor: pointer;
color: #ccc;
}
.recovery-options label:hover { color: var(--primary); }
.recovery-options input[type="checkbox"] {
accent-color: var(--primary);
cursor: pointer;
}
</style>
</head>
<body>
<?php echo auth_render_page_access_widget('scmining.php', 'Scanner Minage'); ?>
<div class="admin-layout">
<header class="admin-topbar">
<div class="topbar-info">
<h1>R.E.A.C.T. Mining Scanner</h1>
<p>Niveau d\'accès : <strong>Administrateur</strong></p>
</div>
<div class="topbar-actions">
<span class="session-user">Connecté : <strong><?php echo htmlspecialchars($current_session_user); ?></strong></span>
<a href="index.php" class="btn-modern">Retour au site</a>
<a href="logout.php" class="btn-modern danger">Déconnexion</a>
</div>
</header>
<?php echo auth_render_app_nav('scmining.php'); ?>
<?php if ($flash_message !== ''): ?>
<div class="flash <?php echo htmlspecialchars($flash_type); ?>">
<?php echo htmlspecialchars($flash_message); ?>
</div>
<?php endif; ?>
<div class="admin-grid">
<!-- Left Column: Search and Add -->
<div class="side-panel">
<section class="glass-card">
<h2>Ajouter un Minéral</h2>
<form method="get" action="scmining.php" style="display: flex; gap: 10px; margin-bottom: 1.5rem;">
<input type="text" name="search" class="form-control" placeholder="Rechercher (ex: Copper)" value="<?php echo htmlspecialchars($search); ?>">
<button type="submit" class="btn-modern">OK</button>
</form>
<?php if ($search !== ''): ?>
<div class="search-results">
<?php if (!empty($configured_matches)): ?>
<div class="search-results-section">
<div class="search-results-caption">Déjà configuré<?php echo count($configured_matches) > 1 ? 's' : ''; ?> pour cette recherche</div>
<?php foreach ($configured_matches as $configured):
if (is_array($configured) && isset($configured['cl_scobjs_uuid']) && isset($configured['cl_scobjs_name']) && isset($configured['cl_scobjs_type']) && isset($configured['cl_scobjs_subtype'])) {
$configured_rarity_code = $configured['cl_scobjs_rarity'] ?? '';
$configured_rarity_class = sc_rarity_class($configured_rarity_code);
?>
<div class="search-result-item is-configured">
<img src="https://cstone.space/uifimages/<?php echo $configured['cl_scobjs_uuid']; ?>.png" class="item-preview" alt="">
<div class="search-result-info">
<span class="search-result-name <?php echo htmlspecialchars($configured_rarity_class); ?>" style="<?php echo htmlspecialchars(sc_rarity_style($configured_rarity_code)); ?>"><?php echo htmlspecialchars($configured['cl_scobjs_name']); ?></span>
<span class="search-result-uuid">UUID: <?php echo htmlspecialchars($configured['cl_scobjs_uuid']); ?></span>
<span class="search-result-meta"><?php echo htmlspecialchars($configured['cl_scobjs_type']); ?> / <?php echo htmlspecialchars($configured['cl_scobjs_subtype']); ?> — déjà présent dans Configuration Minerais</span>
</div>
<span class="search-result-badge">Déjà configuré</span>
</div>
<?php } endforeach; ?>
</div>
<?php endif; ?>
<?php if (empty($search_results)): ?>
<p style="text-align: center; color: #666;">Aucun objet non listé trouvé.</p>
<?php else: ?>
<?php $search_first_result = (($search_page - 1) * $search_per_page) + 1; ?>
<?php $search_last_result = min($search_total_results, $search_first_result + count($search_results) - 1); ?>
<div class="search-results-summary">
Résultats <?php echo $search_first_result; ?> à <?php echo $search_last_result; ?> sur <?php echo $search_total_results; ?> — page <?php echo $search_page; ?>/<?php echo $search_total_pages; ?>
</div>
<?php foreach ($search_results as $res):
if (is_array($res) && isset($res['cl_scobjs_uuid']) && isset($res['cl_scobjs_name']) && isset($res['cl_scobjs_type']) && isset($res['cl_scobjs_subtype']) && isset($res['cl_scobjs_id'])) {
$rarity_code = $res['cl_scobjs_rarity'] ?? '';
$rarity_class = sc_rarity_class($rarity_code);
?>
<div class="search-result-item">
<img src="https://cstone.space/uifimages/<?php echo $res['cl_scobjs_uuid']; ?>.png" class="item-preview" alt="">
<div class="search-result-info">
<span class="search-result-name <?php echo htmlspecialchars($rarity_class); ?>" style="<?php echo htmlspecialchars(sc_rarity_style($rarity_code)); ?>"><?php echo htmlspecialchars($res['cl_scobjs_name']); ?></span>
<span class="search-result-uuid">UUID: <?php echo htmlspecialchars($res['cl_scobjs_uuid']); ?></span>
<span class="search-result-meta"><?php echo htmlspecialchars($res['cl_scobjs_type']); ?> / <?php echo htmlspecialchars($res['cl_scobjs_subtype']); ?></span>
</div>
<form method="post">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>">
<input type="hidden" name="action" value="add_mineral">
<input type="hidden" name="obj_id" value="<?php echo $res['cl_scobjs_id']; ?>">
<input type="hidden" name="return_search" value="<?php echo htmlspecialchars($search); ?>">
<input type="hidden" name="return_page" value="<?php echo $search_page; ?>">
<button type="submit" class="btn-modern btn-mini">+</button>
</form>
</div>
<?php } endforeach; ?>
<?php if ($search_total_pages > 1): ?>
<?php
$page_window_start = max(1, $search_page - 2);
$page_window_end = min($search_total_pages, $search_page + 2);
?>
<div class="pagination-controls">
<?php if ($search_page > 1): ?>
<a href="<?php echo htmlspecialchars(scmining_search_url($search, $search_page - 1)); ?>" class="btn-modern btn-mini">&laquo;</a>
<?php endif; ?>
<?php for ($page_number = $page_window_start; $page_number <= $page_window_end; $page_number++): ?>
<a href="<?php echo htmlspecialchars(scmining_search_url($search, $page_number)); ?>" class="btn-modern btn-mini<?php echo $page_number === $search_page ? ' active' : ''; ?>"><?php echo $page_number; ?></a>
<?php endfor; ?>
<?php if ($search_page < $search_total_pages): ?>
<a href="<?php echo htmlspecialchars(scmining_search_url($search, $search_page + 1)); ?>" class="btn-modern btn-mini">&raquo;</a>
<?php endif; ?>
<span class="pagination-status">Navigation des homonymes activée</span>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
<?php endif; ?>
</section>
</div>
<!-- Right Column: List -->
<main class="main-panel">
<section class="glass-card">
<h2>Configuration Minerais</h2>
<div style="overflow-x: auto;">
<table class="modern-table">
<thead>
<tr>
<th style="width: 60px;">Aperçu</th>
<th>Nom / Type</th>
<th style="text-align: center;">Récupération</th>
<th style="text-align: center;">Valeur Scan</th>
<th style="text-align: center;">Max Occur.</th>
<th style="text-align: right;">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($mining_list)): ?>
<tr><td colspan="6" style="text-align: center; padding: 3rem; color: #666;">Aucun minéral configuré.</td></tr>
<?php else: ?>
<?php foreach ($mining_list as $item):
if (is_array($item) && isset($item['cl_scobjs_uuid']) && isset($item['cl_scobjs_name']) && isset($item['cl_scobjs_type']) && isset($item['cl_scobjs_subtype']) && isset($item['cl_scmining_id']) && isset($item['cl_scmining_scan_value']) && isset($item['cl_scmining_max_occurrence'])) {
$rarity_code = $item['cl_scobjs_rarity'] ?? '';
$rarity_class = sc_rarity_class($rarity_code);
?>
<tr>
<td>
<img src="https://cstone.space/uifimages/<?php echo $item['cl_scobjs_uuid']; ?>.png" class="item-preview" alt="">
</td>
<td>
<strong class="<?php echo htmlspecialchars($rarity_class); ?>" style="<?php echo htmlspecialchars(sc_rarity_style($rarity_code)); ?>"><?php echo htmlspecialchars($item['cl_scobjs_name']); ?></strong><br>
<span style="font-size: 0.75rem; color: #888;"><?php echo htmlspecialchars($item['cl_scobjs_type']); ?> / <?php echo htmlspecialchars($item['cl_scobjs_subtype']); ?></span>
</td>
<form method="post">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>">
<input type="hidden" name="action" value="update_mineral">
<input type="hidden" name="mining_id" value="<?php echo $item['cl_scmining_id']; ?>">
<td style="text-align: center;">
<div class="recovery-options">
<label><input type="checkbox" name="can_manual" <?php echo ($item['cl_scmining_can_manual'] ?? 0) ? 'checked' : ''; ?>> Manuel</label>
<label><input type="checkbox" name="can_land" <?php echo ($item['cl_scmining_can_land'] ?? 0) ? 'checked' : ''; ?>> Terrestre</label>
<label><input type="checkbox" name="can_space" <?php echo ($item['cl_scmining_can_space'] ?? 0) ? 'checked' : ''; ?>> Spatial</label>
</div>
</td>
<td style="text-align: center;">
<input type="number" name="scan_value" class="form-control val-input" required value="<?php echo $item['cl_scmining_scan_value']; ?>">
</td>
<td style="text-align: center;">
<input type="number" name="max_occurrence" class="form-control val-input" required value="<?php echo $item['cl_scmining_max_occurrence']; ?>" min="1" max="10">
</td>
<td style="text-align: right;">
<div style="display: flex; gap: 5px; justify-content: flex-end;">
<button type="submit" class="btn-modern btn-mini">Save</button>
</form>
<form method="post" onsubmit="return confirm('Retirer ce minéral ?');">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>">
<input type="hidden" name="action" value="delete_mineral">
<input type="hidden" name="mining_id" value="<?php echo $item['cl_scmining_id']; ?>">
<button type="submit" class="btn-modern btn-mini danger">X</button>
</form>
</div>
</td>
</tr>
<?php } endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</section>
</main>
</div>
</div>
</body>
</html>

949
scnotification.php Normal file
View File

@ -0,0 +1,949 @@
<?php
require_once __DIR__ . '/db/auth.php';
require_once __DIR__ . '/db/scdiscord.php';
auth_start_session();
auth_bootstrap();
auth_handle_page_access_post('scnotification.php', 'NOTIF DISCORD');
auth_require_page_access('scnotification.php', 'NOTIF DISCORD');
scdiscord_bootstrap();
$db = db();
$csrf_token = auth_csrf_token();
$flash = auth_flash_get();
$flash_type = $flash['type'] ?? '';
$flash_message = $flash['message'] ?? '';
$current_session_user = $_SESSION['user'] ?? '';
$current_session_role = $_SESSION['role'] ?? 'member';
$role_label = ($current_session_role === 'admin') ? 'Administrateur' : 'Membre';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$submitted_csrf = (string) ($_POST['csrf_token'] ?? '');
if (!auth_validate_csrf($submitted_csrf)) {
auth_flash_set('error', 'Jeton CSRF invalide.');
header('Location: scnotification.php');
exit;
}
$_SESSION['scnotification_old'] = $_POST;
$action = (string) ($_POST['action'] ?? '');
$is_legacy_send = $action === '' && (isset($_POST['description']) || isset($_POST['title']) || isset($_POST['webhook_choice']));
if ($action === 'send_notification' || $is_legacy_send) {
$legacy_webhook_choice = trim((string) ($_POST['webhook_choice'] ?? ''));
$cl_scnotification_webhook_id = (int) ($_POST['cl_scnotification_webhook_id'] ?? 0);
if ($cl_scnotification_webhook_id <= 0 && preg_match('/^notif(\d+)$/', $legacy_webhook_choice, $matches)) {
$cl_scnotification_webhook_id = (int) $matches[1];
}
$notify_here = isset($_POST['cl_scnotification_notify_here']) || isset($_POST['ping_here']);
$notify_everyone = isset($_POST['cl_scnotification_notify_everyone']) || isset($_POST['ping_everyone']);
$cl_scnotification_title = trim((string) ($_POST['cl_scnotification_title'] ?? ($_POST['title'] ?? '')));
$cl_scnotification_message = trim((string) ($_POST['cl_scnotification_message'] ?? ($_POST['description'] ?? '')));
$show_footer = isset($_POST['cl_scnotification_show_footer']);
$cl_scnotification_footer_text = trim((string) ($_POST['cl_scnotification_footer_text'] ?? ''));
$cl_scnotification_footer_icon_url = trim((string) ($_POST['cl_scnotification_footer_icon_url'] ?? ''));
$show_reactions = isset($_POST['cl_scnotification_show_reactions']) || isset($_POST['use_reactions']);
$show_thread = isset($_POST['cl_scnotification_show_thread']) || isset($_POST['use_publicthread']);
$show_org = isset($_POST['cl_scnotification_show_org']);
$cl_scnotification_org_value = trim((string) ($_POST['cl_scnotification_org_value'] ?? ''));
$show_pvp = isset($_POST['cl_scnotification_show_pvp']);
$cl_scnotification_pvp_value = trim((string) ($_POST['cl_scnotification_pvp_value'] ?? ''));
$include_schedule = isset($_POST['cl_scnotification_include_schedule']);
$cl_scnotification_location = trim((string) ($_POST['cl_scnotification_location'] ?? ''));
$cl_scnotification_start_date = trim((string) ($_POST['cl_scnotification_start_date'] ?? ''));
$cl_scnotification_departure_time = trim((string) ($_POST['cl_scnotification_departure_time'] ?? ''));
$include_briefing_time = isset($_POST['cl_scnotification_include_briefing_time']);
$cl_scnotification_briefing_time = trim((string) ($_POST['cl_scnotification_briefing_time'] ?? ''));
$include_end_date = isset($_POST['cl_scnotification_include_end_date']);
$cl_scnotification_end_date = trim((string) ($_POST['cl_scnotification_end_date'] ?? ''));
$include_end_time = isset($_POST['cl_scnotification_include_end_time']);
$cl_scnotification_end_time = trim((string) ($_POST['cl_scnotification_end_time'] ?? ''));
$show_channel_url = isset($_POST['cl_scnotification_show_channel_url']);
$cl_scnotification_channel_url = trim((string) ($_POST['cl_scnotification_channel_url'] ?? ''));
$show_inventory_url = isset($_POST['cl_scnotification_show_inventory_url']);
$cl_scnotification_inventory_url = trim((string) ($_POST['cl_scnotification_inventory_url'] ?? ''));
$show_source_url = isset($_POST['cl_scnotification_show_source_url']);
$cl_scnotification_source_url = trim((string) ($_POST['cl_scnotification_source_url'] ?? ''));
if ($cl_scnotification_webhook_id <= 0) {
auth_flash_set('error', 'Veuillez sélectionner un webhook Discord.');
header('Location: scnotification.php');
exit;
}
if ($cl_scnotification_message === '') {
auth_flash_set('error', 'Le champ description est obligatoire pour envoyer la notification.');
header('Location: scnotification.php');
exit;
}
$stmt_webhook = $db->prepare('SELECT * FROM tbl_scwebhooks WHERE cl_scwebhook_id = :id LIMIT 1');
$stmt_webhook->execute(['id' => $cl_scnotification_webhook_id]);
$webhook = $stmt_webhook->fetch();
if (!$webhook) {
auth_flash_set('error', 'Webhook Discord introuvable.');
header('Location: scnotification.php');
exit;
}
foreach ([
'URL canal Discord' => [$show_channel_url, $cl_scnotification_channel_url],
'URL inventaire' => [$show_inventory_url, $cl_scnotification_inventory_url],
'URL source' => [$show_source_url, $cl_scnotification_source_url],
'Icône du footer' => [$show_footer && $cl_scnotification_footer_icon_url !== '', $cl_scnotification_footer_icon_url],
] as $label => [$enabled, $value]) {
if ($enabled && $value !== '' && !filter_var($value, FILTER_VALIDATE_URL)) {
auth_flash_set('error', $label . ' invalide.');
header('Location: scnotification.php');
exit;
}
}
$mentions = scdiscord_build_mentions($notify_here, $notify_everyone);
$banner_image_url = (string) ($webhook['cl_scwebhook_image_url'] ?? '');
$border_color = (string) ($webhook['cl_scwebhook_border_color'] ?? '#ffae00');
$fields = [];
if ($show_org && $cl_scnotification_org_value !== '' && mb_strtolower($cl_scnotification_org_value) !== 'non') {
$fields[] = [
'name' => 'Tenue dorganisation',
'value' => mb_substr($cl_scnotification_org_value, 0, 1024),
'inline' => true,
];
}
if ($show_pvp && $cl_scnotification_pvp_value !== '' && mb_strtolower($cl_scnotification_pvp_value) !== 'inexistant') {
$fields[] = [
'name' => 'Risques PvP',
'value' => mb_substr($cl_scnotification_pvp_value, 0, 1024),
'inline' => true,
];
}
if ($include_schedule) {
if ($cl_scnotification_location !== '') {
$fields[] = ['name' => 'Lieu de ralliement', 'value' => mb_substr($cl_scnotification_location, 0, 1024), 'inline' => false];
}
if ($cl_scnotification_start_date !== '') {
$fields[] = ['name' => 'Date de début', 'value' => mb_substr($cl_scnotification_start_date, 0, 1024), 'inline' => true];
}
if ($cl_scnotification_departure_time !== '') {
$fields[] = ['name' => 'Heure de départ', 'value' => mb_substr($cl_scnotification_departure_time, 0, 1024), 'inline' => true];
}
if ($include_briefing_time && $cl_scnotification_briefing_time !== '') {
$fields[] = ['name' => 'Heure de briefing', 'value' => mb_substr($cl_scnotification_briefing_time, 0, 1024), 'inline' => true];
}
if ($include_end_date && $cl_scnotification_end_date !== '') {
$fields[] = ['name' => 'Date de fin', 'value' => mb_substr($cl_scnotification_end_date, 0, 1024), 'inline' => true];
}
if ($include_end_time && $cl_scnotification_end_time !== '') {
$fields[] = ['name' => 'Heure de fin', 'value' => mb_substr($cl_scnotification_end_time, 0, 1024), 'inline' => true];
}
}
if ($show_channel_url && $cl_scnotification_channel_url !== '') {
$fields[] = ['name' => 'Canal Discord', 'value' => '[Ouvrir le canal](' . $cl_scnotification_channel_url . ')', 'inline' => false];
}
if ($show_inventory_url && $cl_scnotification_inventory_url !== '') {
$fields[] = ['name' => 'Inventaire A.R.I.A', 'value' => '[Consulter linventaire](' . $cl_scnotification_inventory_url . ')', 'inline' => false];
}
if ($show_source_url && $cl_scnotification_source_url !== '') {
$fields[] = ['name' => 'Source', 'value' => '[Ouvrir la source](' . $cl_scnotification_source_url . ')', 'inline' => false];
}
$embed = [
'description' => mb_substr($cl_scnotification_message, 0, 4096),
'color' => scdiscord_hex_to_decimal($border_color),
];
if ($cl_scnotification_title !== '') {
$embed['title'] = mb_substr($cl_scnotification_title, 0, 256);
}
if (!empty($fields)) {
$embed['fields'] = $fields;
}
if ($banner_image_url !== '') {
$embed['image'] = ['url' => $banner_image_url];
}
if ($show_footer && ($cl_scnotification_footer_text !== '' || $cl_scnotification_footer_icon_url !== '')) {
$embed['footer'] = [];
if ($cl_scnotification_footer_text !== '') {
$embed['footer']['text'] = mb_substr($cl_scnotification_footer_text, 0, 2048);
}
if ($cl_scnotification_footer_icon_url !== '') {
$embed['footer']['icon_url'] = $cl_scnotification_footer_icon_url;
}
}
$payload = [
'embeds' => [$embed],
];
if (!empty($mentions)) {
$payload['content'] = implode(' ', $mentions);
$payload['allowed_mentions'] = ['parse' => ['everyone']];
}
$thread_name = $show_thread
? ('Discussion - ' . scdiscord_build_thread_name(
$cl_scnotification_title,
$cl_scnotification_location,
$cl_scnotification_start_date
))
: '';
$result = scdiscord_post_webhook((string) $webhook['cl_scwebhook_url'], $payload);
$bot_actions = [
'success' => true,
'http_code' => 200,
'response' => 'Aucune action bot demandée.',
];
if (!empty($result['success']) && ($show_reactions || $show_thread)) {
$message_data = scdiscord_decode_json_response((string) ($result['response'] ?? ''));
$bot_actions = scdiscord_apply_bot_actions($message_data, $show_reactions, $show_thread, $thread_name);
}
$log_response = json_encode([
'webhook' => [
'success' => !empty($result['success']),
'http_code' => (int) ($result['http_code'] ?? 0),
'response' => (string) ($result['response'] ?? ''),
],
'bot_actions' => [
'success' => !empty($bot_actions['success']),
'http_code' => (int) ($bot_actions['http_code'] ?? 0),
'response' => (string) ($bot_actions['response'] ?? ''),
'details' => $bot_actions['details'] ?? [],
],
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$stmt_log = $db->prepare(
'INSERT INTO tbl_scnotifications (
cl_scnotification_webhook_id,
cl_scnotification_title,
cl_scnotification_message,
cl_scnotification_payload,
cl_scnotification_response,
cl_scnotification_success,
cl_scnotification_created_by
) VALUES (
:webhook_id,
:title,
:message,
:payload,
:response,
:success,
:created_by
)'
);
$stmt_log->execute([
'webhook_id' => $cl_scnotification_webhook_id,
'title' => $cl_scnotification_title,
'message' => $cl_scnotification_message,
'payload' => json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
'response' => $log_response !== false ? $log_response : (string) ($result['response'] ?? ''),
'success' => (!empty($result['success']) && !empty($bot_actions['success'])) ? 1 : 0,
'created_by' => $current_session_user !== '' ? $current_session_user : 'Inconnu',
]);
if (empty($result['success'])) {
auth_flash_set('error', 'Échec de lenvoi Discord (HTTP ' . (int) ($result['http_code'] ?? 0) . ').');
} elseif (empty($bot_actions['success'])) {
auth_flash_set('error', 'Notification envoyée, mais échec des actions bot Discord : ' . (string) ($bot_actions['response'] ?? 'Erreur inconnue.'));
} else {
unset($_SESSION['scnotification_old']);
auth_flash_set('success', 'Notification Discord envoyée avec succès.');
}
header('Location: scnotification.php');
exit;
}
}
$old = $_SESSION['scnotification_old'] ?? [];
unset($_SESSION['scnotification_old']);
$stmt_webhooks = $db->query('SELECT * FROM tbl_scwebhooks ORDER BY cl_scwebhook_name ASC');
$webhooks = $stmt_webhooks->fetchAll();
function scnotification_old_value(array $old, string $key, string $default = ''): string
{
$value = $old[$key] ?? $default;
return is_string($value) ? $value : $default;
}
function scnotification_old_checked(array $old, string $key, bool $default = false): bool
{
if (empty($old)) {
return $default;
}
return array_key_exists($key, $old);
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SC Notification | R.E.A.C.T. Admin</title>
<link rel="stylesheet" type="text/css" href="css/styles.css">
<link rel="stylesheet" type="text/css" href="css/default.css">
<style>
:root {
--primary: #a29b78;
--primary-glow: rgba(162, 155, 120, 0.4);
--bg-dark: #080a0f;
--card-bg: rgba(20, 24, 33, 0.85);
--border-glow: rgba(162, 155, 120, 0.25);
--danger: #ff4d4d;
--success: #00ff88;
--text-soft: #d7d7d7;
--text-muted: #9ea3ad;
}
@font-face {
font-family: 'Electrolize';
src: url('fonts/Electrolize-Regular.ttf') format('truetype');
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background: radial-gradient(circle at top right, #1a1f2e, var(--bg-dark));
background-attachment: fixed;
color: #e0e0e0;
font-family: 'Electrolize', sans-serif;
overflow-x: hidden;
}
.admin-layout {
display: flex;
flex-direction: column;
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
animation: fadeIn 0.6s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.admin-topbar,
.page-shell,
.history-card {
background: var(--card-bg);
backdrop-filter: blur(12px);
border: 1px solid var(--border-glow);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.admin-topbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
padding: 1.5rem 2rem;
margin-bottom: 2rem;
}
.topbar-info h1 {
margin: 0;
font-size: 1.5rem;
letter-spacing: 2px;
text-transform: uppercase;
background: linear-gradient(90deg, #fff, var(--primary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.topbar-info p {
margin: 0.25rem 0 0;
font-size: 0.85rem;
color: var(--primary);
opacity: 0.8;
}
.topbar-actions {
display: flex;
gap: 1rem;
flex-wrap: wrap;
align-items: center;
}
.session-user {
opacity: 0.85;
}
.btn-modern {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
padding: 0.6rem 1.2rem;
border-radius: 4px;
border: 1px solid var(--primary);
background: transparent;
color: #fff;
text-decoration: none;
text-transform: uppercase;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-family: 'Electrolize', sans-serif;
}
.btn-modern:hover {
background: var(--primary);
color: var(--bg-dark);
box-shadow: 0 0 15px var(--primary-glow);
}
.btn-modern.danger {
border-color: var(--danger);
color: var(--danger);
}
.btn-modern.danger:hover {
background: var(--danger);
color: #fff;
box-shadow: 0 0 15px rgba(255, 77, 77, 0.3);
}
.nav-tabs {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border-glow);
}
.nav-tabs a {
text-decoration: none;
color: #888;
text-transform: uppercase;
font-size: 0.9rem;
transition: color 0.3s;
}
.nav-tabs a:hover,
.nav-tabs a.active {
color: var(--primary);
}
.flash {
margin-bottom: 1rem;
padding: 1rem 1.15rem;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.04);
}
.flash.success { color: #9ff3c8; border-color: rgba(0, 255, 136, 0.25); }
.flash.error { color: #ff9d9d; border-color: rgba(255, 77, 77, 0.25); }
.page-shell {
padding: 1.5rem;
}
.page-title {
margin: 0 0 1.5rem;
padding-bottom: 0.75rem;
font-size: 1.25rem;
color: var(--primary);
border-bottom: 1px solid var(--border-glow);
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.form-column {
display: grid;
gap: 1rem;
}
.section-card {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 12px;
padding: 1.25rem;
}
.section-title {
margin: 0 0 1rem;
padding-bottom: 0.65rem;
color: var(--primary);
font-size: 1rem;
line-height: 1.2;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.field-row { margin-bottom: 0.85rem; }
.field-row:last-child { margin-bottom: 0; }
label, .inline-label {
display: block;
margin-bottom: 0.45rem;
font-size: 0.83rem;
color: #aaa;
text-transform: uppercase;
}
.check {
display: flex;
align-items: center;
gap: 0.55rem;
font-size: 0.9rem;
margin-bottom: 0.7rem;
color: #ededed;
text-transform: none;
}
.control,
textarea,
select,
input[type="text"],
input[type="url"],
input[type="date"],
input[type="time"] {
width: 100%;
padding: 0.8rem 1rem;
border-radius: 4px;
border: 1px solid #444;
background: rgba(0, 0, 0, 0.3);
color: #fff;
font-family: 'Electrolize', sans-serif;
transition: border-color 0.3s, background 0.3s;
}
.control:focus,
textarea:focus,
select:focus,
input[type="text"]:focus,
input[type="url"]:focus,
input[type="date"]:focus,
input[type="time"]:focus {
outline: none;
border-color: var(--primary);
background: rgba(0, 0, 0, 0.5);
}
select.control {
background: #353b45;
color: #fff;
border-color: #565d68;
color-scheme: dark;
}
select.control:focus {
background: #3d444f;
color: #fff;
}
select.control option {
background: #353b45;
color: #fff;
}
select.control option:checked {
background: #4a5260;
color: #fff;
}
textarea {
min-height: 180px;
resize: vertical;
}
input[disabled], select[disabled], textarea[disabled] {
opacity: 0.5;
cursor: not-allowed;
}
.subgrid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.8rem;
}
.char-counter {
margin-top: 0.3rem;
font-size: 0.72rem;
color: var(--text-muted);
text-align: right;
}
.submit-wrap {
margin-top: 1rem;
}
.submit-wrap .btn-modern {
width: 100%;
padding: 0.95rem 1.2rem;
}
.history-card {
margin-top: 1.5rem;
padding: 1.2rem;
}
.history-card h2 {
margin: 0 0 1rem;
padding-bottom: 0.75rem;
color: var(--primary);
font-size: 1.1rem;
text-transform: uppercase;
border-bottom: 1px solid var(--border-glow);
}
.history-list {
display: grid;
gap: 0.7rem;
}
.history-item {
padding: 0.85rem 0.95rem;
border-radius: 8px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.history-meta {
margin-top: 0.25rem;
color: var(--text-muted);
font-size: 0.78rem;
}
.status-pill {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.2rem 0.55rem;
border-radius: 999px;
font-size: 0.72rem;
text-transform: uppercase;
}
.status-pill.ok {
background: rgba(0, 255, 136, 0.12);
color: #9ff3c8;
}
.status-pill.ko {
background: rgba(255, 77, 77, 0.12);
color: #ffb0b0;
}
.empty-state {
padding: 1.2rem;
border: 1px dashed rgba(255, 255, 255, 0.12);
border-radius: 10px;
text-align: center;
color: var(--text-muted);
}
@media (max-width: 1024px) {
.form-grid, .subgrid-2 { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<?php echo auth_render_page_access_widget('scnotification.php', 'NOTIF DISCORD'); ?>
<div class="admin-layout">
<header class="admin-topbar">
<div class="topbar-info">
<h1>R.E.A.C.T. SC Notification</h1>
<p>Niveau d'accès : <strong><?php echo htmlspecialchars($role_label, ENT_QUOTES, 'UTF-8'); ?></strong></p>
</div>
<div class="topbar-actions">
<span class="session-user">Connecté : <strong><?php echo htmlspecialchars($current_session_user, ENT_QUOTES, 'UTF-8'); ?></strong></span>
<a href="index.php" class="btn-modern">Retour au site</a>
<a href="logout.php" class="btn-modern danger">Déconnexion</a>
</div>
</header>
<?php echo auth_render_app_nav('scnotification.php'); ?>
<?php if ($flash_message !== ''): ?>
<div class="flash <?php echo htmlspecialchars($flash_type, ENT_QUOTES, 'UTF-8'); ?>">
<?php echo htmlspecialchars($flash_message, ENT_QUOTES, 'UTF-8'); ?>
</div>
<?php endif; ?>
<div class="page-shell">
<div class="page-title">Envoi d'une notification sur Discord</div>
<form method="post">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="action" value="send_notification">
<div class="form-grid">
<div class="form-column">
<section class="section-card">
<h2 class="section-title">Canal de notification</h2>
<div class="field-row">
<select class="control" name="cl_scnotification_webhook_id" required>
<option value="">Sélectionner un webhook</option>
<?php foreach ($webhooks as $webhook): ?>
<?php $selected = (string) $webhook['cl_scwebhook_id'] === scnotification_old_value($old, 'cl_scnotification_webhook_id'); ?>
<option value="<?php echo (int) $webhook['cl_scwebhook_id']; ?>" <?php echo $selected ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($webhook['cl_scwebhook_name'] . (((int) $webhook['cl_scwebhook_is_forum'] === 1) ? ' [forum]' : ''), ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
</div>
</section>
<section class="section-card">
<h2 class="section-title">Prévisualisation et Couleur</h2>
<p style="margin:0; color: var(--text-muted); line-height:1.6;">
Limage de prévisualisation et la couleur de bordure sont maintenant prises automatiquement depuis le canal Discord sélectionné.
Pour les modifier, va dans la page de configuration Discord du webhook correspondant.
</p>
</section>
<section class="section-card">
<h2 class="section-title">Message</h2>
<label class="check">
<input type="checkbox" name="cl_scnotification_notify_here" value="1" <?php echo scnotification_old_checked($old, 'cl_scnotification_notify_here') ? 'checked' : ''; ?>>
<span>Notifier avec @here</span>
</label>
<label class="check">
<input type="checkbox" name="cl_scnotification_notify_everyone" value="1" <?php echo scnotification_old_checked($old, 'cl_scnotification_notify_everyone', true) ? 'checked' : ''; ?>>
<span>Notifier avec @everyone</span>
</label>
<div class="field-row">
<label for="cl_scnotification_title">Titre :</label>
<input type="text" class="control" id="cl_scnotification_title" name="cl_scnotification_title" value="<?php echo htmlspecialchars(scnotification_old_value($old, 'cl_scnotification_title'), ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="field-row">
<label for="cl_scnotification_message">Description :</label>
<textarea id="cl_scnotification_message" name="cl_scnotification_message" maxlength="2500" placeholder="Compatible avec le Markdown Discord..."><?php echo htmlspecialchars(scnotification_old_value($old, 'cl_scnotification_message'), ENT_QUOTES, 'UTF-8'); ?></textarea>
<div class="char-counter"><span id="messageCount">2500</span> caractères restants</div>
</div>
</section>
<section class="section-card">
<h2 class="section-title">Options Footer</h2>
<label class="check">
<input type="checkbox" name="cl_scnotification_show_footer" id="cl_scnotification_show_footer" value="1" <?php echo scnotification_old_checked($old, 'cl_scnotification_show_footer', true) ? 'checked' : ''; ?>>
<span>Afficher le footer</span>
</label>
<div class="field-row">
<label for="cl_scnotification_footer_text">Texte du footer :</label>
<input type="text" class="control footer-toggle" id="cl_scnotification_footer_text" name="cl_scnotification_footer_text" value="<?php echo htmlspecialchars(scnotification_old_value($old, 'cl_scnotification_footer_text', 'R.E.A.C.T Initiative'), ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="field-row">
<label for="cl_scnotification_footer_icon_url">URL icône footer :</label>
<input type="url" class="control footer-toggle" id="cl_scnotification_footer_icon_url" name="cl_scnotification_footer_icon_url" value="<?php echo htmlspecialchars(scnotification_old_value($old, 'cl_scnotification_footer_icon_url'), ENT_QUOTES, 'UTF-8'); ?>" placeholder="*.png">
</div>
</section>
<section class="section-card">
<h2 class="section-title">Réactions &amp; Fils</h2>
<label class="check">
<input type="checkbox" name="cl_scnotification_show_reactions" value="1" <?php echo scnotification_old_checked($old, 'cl_scnotification_show_reactions') ? 'checked' : ''; ?>>
<span>Afficher les réactions 👍 / / / 👎</span>
</label>
<label class="check">
<input type="checkbox" name="cl_scnotification_show_thread" value="1" <?php echo scnotification_old_checked($old, 'cl_scnotification_show_thread', true) ? 'checked' : ''; ?>>
<span>Afficher le fil de discussion</span>
</label>
</section>
</div>
<div class="form-column">
<section class="section-card">
<h2 class="section-title">Tenue d'organisation &amp; PvP</h2>
<label class="check">
<input type="checkbox" name="cl_scnotification_show_org" id="cl_scnotification_show_org" value="1" <?php echo scnotification_old_checked($old, 'cl_scnotification_show_org') ? 'checked' : ''; ?>>
<span>Afficher la tenue d'organisation</span>
</label>
<div class="field-row">
<select class="control org-toggle" name="cl_scnotification_org_value" id="cl_scnotification_org_value">
<?php foreach (['Non', 'Tenue civile', 'Tenue organisation', 'Tenue lourde'] as $option): ?>
<option value="<?php echo htmlspecialchars($option, ENT_QUOTES, 'UTF-8'); ?>" <?php echo scnotification_old_value($old, 'cl_scnotification_org_value', 'Non') === $option ? 'selected' : ''; ?>><?php echo htmlspecialchars($option, ENT_QUOTES, 'UTF-8'); ?></option>
<?php endforeach; ?>
</select>
</div>
<label class="check">
<input type="checkbox" name="cl_scnotification_show_pvp" id="cl_scnotification_show_pvp" value="1" <?php echo scnotification_old_checked($old, 'cl_scnotification_show_pvp') ? 'checked' : ''; ?>>
<span>Afficher les risques PvP</span>
</label>
<div class="field-row">
<select class="control pvp-toggle" name="cl_scnotification_pvp_value" id="cl_scnotification_pvp_value">
<?php foreach (['Inexistant', 'Faible', 'Modéré', 'Important', 'Extrême'] as $option): ?>
<option value="<?php echo htmlspecialchars($option, ENT_QUOTES, 'UTF-8'); ?>" <?php echo scnotification_old_value($old, 'cl_scnotification_pvp_value', 'Inexistant') === $option ? 'selected' : ''; ?>><?php echo htmlspecialchars($option, ENT_QUOTES, 'UTF-8'); ?></option>
<?php endforeach; ?>
</select>
</div>
</section>
<section class="section-card">
<h2 class="section-title">Lieu, Date et Heure</h2>
<label class="check">
<input type="checkbox" name="cl_scnotification_include_schedule" id="cl_scnotification_include_schedule" value="1" <?php echo scnotification_old_checked($old, 'cl_scnotification_include_schedule') ? 'checked' : ''; ?>>
<span>Inclure date, lieu et heures</span>
</label>
<div class="field-row">
<label for="cl_scnotification_location">Lieu de ralliement :</label>
<input type="text" class="control schedule-toggle" id="cl_scnotification_location" name="cl_scnotification_location" value="<?php echo htmlspecialchars(scnotification_old_value($old, 'cl_scnotification_location'), ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="field-row subgrid-2">
<div>
<label for="cl_scnotification_start_date">Date de début :</label>
<input type="date" class="control schedule-toggle" id="cl_scnotification_start_date" name="cl_scnotification_start_date" value="<?php echo htmlspecialchars(scnotification_old_value($old, 'cl_scnotification_start_date'), ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div>
<label for="cl_scnotification_departure_time">Heure de départ :</label>
<input type="time" class="control schedule-toggle" id="cl_scnotification_departure_time" name="cl_scnotification_departure_time" value="<?php echo htmlspecialchars(scnotification_old_value($old, 'cl_scnotification_departure_time', '21:30'), ENT_QUOTES, 'UTF-8'); ?>">
</div>
</div>
<label class="check">
<input type="checkbox" name="cl_scnotification_include_briefing_time" id="cl_scnotification_include_briefing_time" value="1" <?php echo scnotification_old_checked($old, 'cl_scnotification_include_briefing_time') ? 'checked' : ''; ?>>
<span>Inclure une heure de briefing</span>
</label>
<div class="field-row">
<label for="cl_scnotification_briefing_time">Heure de briefing :</label>
<input type="time" class="control briefing-toggle" id="cl_scnotification_briefing_time" name="cl_scnotification_briefing_time" value="<?php echo htmlspecialchars(scnotification_old_value($old, 'cl_scnotification_briefing_time', '21:00'), ENT_QUOTES, 'UTF-8'); ?>">
</div>
<label class="check">
<input type="checkbox" name="cl_scnotification_include_end_date" id="cl_scnotification_include_end_date" value="1" <?php echo scnotification_old_checked($old, 'cl_scnotification_include_end_date') ? 'checked' : ''; ?>>
<span>Inclure une date de fin</span>
</label>
<div class="field-row">
<label for="cl_scnotification_end_date">Date de fin :</label>
<input type="date" class="control enddate-toggle" id="cl_scnotification_end_date" name="cl_scnotification_end_date" value="<?php echo htmlspecialchars(scnotification_old_value($old, 'cl_scnotification_end_date'), ENT_QUOTES, 'UTF-8'); ?>">
</div>
<label class="check">
<input type="checkbox" name="cl_scnotification_include_end_time" id="cl_scnotification_include_end_time" value="1" <?php echo scnotification_old_checked($old, 'cl_scnotification_include_end_time') ? 'checked' : ''; ?>>
<span>Inclure une heure de fin</span>
</label>
<div class="field-row">
<label for="cl_scnotification_end_time">Heure de fin :</label>
<input type="time" class="control endtime-toggle" id="cl_scnotification_end_time" name="cl_scnotification_end_time" value="<?php echo htmlspecialchars(scnotification_old_value($old, 'cl_scnotification_end_time', '00:00'), ENT_QUOTES, 'UTF-8'); ?>">
</div>
</section>
<section class="section-card">
<h2 class="section-title">URL externes</h2>
<label class="check">
<input type="checkbox" name="cl_scnotification_show_channel_url" id="cl_scnotification_show_channel_url" value="1" <?php echo scnotification_old_checked($old, 'cl_scnotification_show_channel_url') ? 'checked' : ''; ?>>
<span>Afficher un lien de canal Discord</span>
</label>
<div class="field-row">
<label for="cl_scnotification_channel_url">URL Canal :</label>
<input type="url" class="control channelurl-toggle" id="cl_scnotification_channel_url" name="cl_scnotification_channel_url" value="<?php echo htmlspecialchars(scnotification_old_value($old, 'cl_scnotification_channel_url', 'https://discord.com/channels/...'), ENT_QUOTES, 'UTF-8'); ?>">
</div>
<label class="check">
<input type="checkbox" name="cl_scnotification_show_inventory_url" id="cl_scnotification_show_inventory_url" value="1" <?php echo scnotification_old_checked($old, 'cl_scnotification_show_inventory_url') ? 'checked' : ''; ?>>
<span>Afficher une URL de l'inventaire A.R.I.A</span>
</label>
<div class="field-row">
<label for="cl_scnotification_inventory_url">URL Inventaire :</label>
<input type="url" class="control inventoryurl-toggle" id="cl_scnotification_inventory_url" name="cl_scnotification_inventory_url" value="<?php echo htmlspecialchars(scnotification_old_value($old, 'cl_scnotification_inventory_url', 'https://aria.blackops-agency.fr/...'), ENT_QUOTES, 'UTF-8'); ?>">
</div>
<label class="check">
<input type="checkbox" name="cl_scnotification_show_source_url" id="cl_scnotification_show_source_url" value="1" <?php echo scnotification_old_checked($old, 'cl_scnotification_show_source_url') ? 'checked' : ''; ?>>
<span>Afficher une URL source</span>
</label>
<div class="field-row">
<label for="cl_scnotification_source_url">URL Source :</label>
<input type="url" class="control sourceurl-toggle" id="cl_scnotification_source_url" name="cl_scnotification_source_url" value="<?php echo htmlspecialchars(scnotification_old_value($old, 'cl_scnotification_source_url', 'https://...'), ENT_QUOTES, 'UTF-8'); ?>">
</div>
</section>
</div>
</div>
<div class="submit-wrap">
<button type="submit" class="btn-modern">Envoyer</button>
</div>
</form>
</div>
</div>
<script>
const messageField = document.getElementById('cl_scnotification_message');
const messageCount = document.getElementById('messageCount');
function updateCounter() {
const remaining = 2500 - (messageField.value || '').length;
messageCount.textContent = remaining;
}
function toggleByCheckbox(checkboxId, selector, invert = false) {
const checkbox = document.getElementById(checkboxId);
const targets = document.querySelectorAll(selector);
if (!checkbox) return;
const enabled = invert ? !checkbox.checked : checkbox.checked;
targets.forEach((target) => {
target.disabled = !enabled;
});
}
function syncStates() {
toggleByCheckbox('cl_scnotification_show_footer', '.footer-toggle');
toggleByCheckbox('cl_scnotification_show_org', '.org-toggle');
toggleByCheckbox('cl_scnotification_show_pvp', '.pvp-toggle');
toggleByCheckbox('cl_scnotification_include_schedule', '.schedule-toggle');
const scheduleEnabled = document.getElementById('cl_scnotification_include_schedule')?.checked;
document.getElementById('cl_scnotification_include_briefing_time').disabled = !scheduleEnabled;
document.getElementById('cl_scnotification_include_end_date').disabled = !scheduleEnabled;
document.getElementById('cl_scnotification_include_end_time').disabled = !scheduleEnabled;
toggleByCheckbox('cl_scnotification_include_briefing_time', '.briefing-toggle');
toggleByCheckbox('cl_scnotification_include_end_date', '.enddate-toggle');
toggleByCheckbox('cl_scnotification_include_end_time', '.endtime-toggle');
if (!scheduleEnabled) {
document.querySelectorAll('.briefing-toggle, .enddate-toggle, .endtime-toggle').forEach(el => el.disabled = true);
}
toggleByCheckbox('cl_scnotification_show_channel_url', '.channelurl-toggle');
toggleByCheckbox('cl_scnotification_show_inventory_url', '.inventoryurl-toggle');
toggleByCheckbox('cl_scnotification_show_source_url', '.sourceurl-toggle');
}
updateCounter();
messageField.addEventListener('input', updateCounter);
[
'cl_scnotification_show_footer',
'cl_scnotification_show_org',
'cl_scnotification_show_pvp',
'cl_scnotification_include_schedule',
'cl_scnotification_include_briefing_time',
'cl_scnotification_include_end_date',
'cl_scnotification_include_end_time',
'cl_scnotification_show_channel_url',
'cl_scnotification_show_inventory_url',
'cl_scnotification_show_source_url'
].forEach((id) => {
const el = document.getElementById(id);
if (el) {
el.addEventListener('change', syncStates);
}
});
syncStates();
</script>
</body>
</html>

693
scpreset.php Normal file
View File

@ -0,0 +1,693 @@
<?php
require_once __DIR__ . '/db/auth.php';
auth_start_session();
auth_bootstrap();
auth_handle_page_access_post('scpreset.php', 'Presets Vaisseau');
auth_require_page_access('scpreset.php', 'Presets Vaisseau');
$flash = auth_flash_get();
$flash_type = $flash['type'] ?? '';
$flash_message = $flash['message'] ?? '';
$db = db();
$csrf_token = auth_csrf_token();
$current_session_user = $_SESSION['user'] ?? '';
$current_session_role = $_SESSION['role'] ?? 'member';
$role_label = ($current_session_role === 'admin') ? 'Administrateur' : 'Membre';
function normalize_catalog_label(string $value): string {
$value = trim($value);
return function_exists('mb_strtolower') ? mb_strtolower($value, 'UTF-8') : strtolower($value);
}
function find_preset_ship_relation(PDO $db, int $manufactureId, int $shipId): ?array {
if ($manufactureId <= 0 || $shipId <= 0) {
return null;
}
$stmt = $db->prepare(
"SELECT
m.cl_scmanufactures_id,
m.cl_scmanufactures_name,
v.cl_scvaisseaux_id,
v.cl_scvaisseaux_name
FROM tbl_scvaisseaux v
INNER JOIN tbl_scmanufactures m
ON m.cl_scmanufactures_id = v.cl_scvaisseaux_manufacture_id
WHERE m.cl_scmanufactures_id = :manufacture_id
AND v.cl_scvaisseaux_id = :ship_id
LIMIT 1"
);
$stmt->execute([
'manufacture_id' => $manufactureId,
'ship_id' => $shipId,
]);
$relation = $stmt->fetch();
return $relation ?: null;
}
// Handle POST actions
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$submitted_csrf = $_POST['csrf_token'] ?? '';
if (!auth_validate_csrf($submitted_csrf)) {
auth_flash_set('error', 'Jeton CSRF invalide.');
header('Location: scpreset.php');
exit;
}
$action = $_POST['action'] ?? '';
// Add preset
if ($action === 'add_preset') {
$manufacture_id = (int)($_POST['manufacture_id'] ?? 0);
$ship_id = (int)($_POST['ship_id'] ?? 0);
$description = trim($_POST['description'] ?? '');
$link = trim($_POST['link'] ?? '');
$creator = $current_session_user ?: 'Inconnu';
$relation = find_preset_ship_relation($db, $manufacture_id, $ship_id);
if ($relation && $link !== '') {
try {
$stmt = $db->prepare("INSERT INTO tbl_scpreset (
cl_scpreset_manufacture_id,
cl_scpreset_vaisseau_id,
cl_scpreset_name,
cl_scpreset_manufacturer,
cl_scpreset_description,
cl_scpreset_link,
cl_scpreset_creator
) VALUES (
:manufacture_id,
:ship_id,
:name,
:manufacturer,
:description,
:link,
:creator
)");
$stmt->execute([
'manufacture_id' => $relation['cl_scmanufactures_id'],
'ship_id' => $relation['cl_scvaisseaux_id'],
'name' => $relation['cl_scvaisseaux_name'],
'manufacturer' => $relation['cl_scmanufactures_name'],
'description' => $description,
'link' => $link,
'creator' => $creator,
]);
auth_flash_set('success', 'Preset ajouté avec succès.');
} catch (PDOException $e) {
auth_flash_set('error', 'Erreur lors de l\'ajout : ' . $e->getMessage());
}
} else {
auth_flash_set('error', 'Veuillez sélectionner une manufacture, un vaisseau valide et renseigner le lien.');
}
header('Location: scpreset.php');
exit;
}
// Update preset
if ($action === 'update_preset') {
$preset_id = (int)($_POST['preset_id'] ?? 0);
$manufacture_id = (int)($_POST['manufacture_id'] ?? 0);
$ship_id = (int)($_POST['ship_id'] ?? 0);
$description = trim($_POST['description'] ?? '');
$link = trim($_POST['link'] ?? '');
$relation = find_preset_ship_relation($db, $manufacture_id, $ship_id);
if ($preset_id > 0 && $relation && $link !== '') {
try {
$stmt = $db->prepare("UPDATE tbl_scpreset SET
cl_scpreset_manufacture_id = :manufacture_id,
cl_scpreset_vaisseau_id = :ship_id,
cl_scpreset_name = :name,
cl_scpreset_manufacturer = :manufacturer,
cl_scpreset_description = :description,
cl_scpreset_link = :link
WHERE cl_scpreset_id = :id");
$stmt->execute([
'manufacture_id' => $relation['cl_scmanufactures_id'],
'ship_id' => $relation['cl_scvaisseaux_id'],
'name' => $relation['cl_scvaisseaux_name'],
'manufacturer' => $relation['cl_scmanufactures_name'],
'description' => $description,
'link' => $link,
'id' => $preset_id,
]);
auth_flash_set('success', 'Preset mis à jour.');
} catch (PDOException $e) {
auth_flash_set('error', 'Erreur lors de la mise à jour : ' . $e->getMessage());
}
} else {
auth_flash_set('error', 'Données invalides : sélectionne une manufacture, un vaisseau valide et un lien.');
}
header('Location: scpreset.php');
exit;
}
// Delete preset
if ($action === 'delete_preset') {
$preset_id = (int)($_POST['preset_id'] ?? 0);
if ($preset_id > 0) {
$stmt = $db->prepare("DELETE FROM tbl_scpreset WHERE cl_scpreset_id = :id");
$stmt->execute(['id' => $preset_id]);
auth_flash_set('success', 'Preset supprimé.');
} else {
auth_flash_set('error', 'ID de preset invalide.');
}
header('Location: scpreset.php');
exit;
}
}
$stmt_mans = $db->query("SELECT * FROM tbl_scmanufactures ORDER BY cl_scmanufactures_name ASC");
$manufactures = $stmt_mans->fetchAll();
$stmt_ships = $db->query("SELECT
v.cl_scvaisseaux_id,
v.cl_scvaisseaux_name,
v.cl_scvaisseaux_manufacture_id,
m.cl_scmanufactures_name
FROM tbl_scvaisseaux v
INNER JOIN tbl_scmanufactures m ON m.cl_scmanufactures_id = v.cl_scvaisseaux_manufacture_id
ORDER BY m.cl_scmanufactures_name ASC, v.cl_scvaisseaux_name ASC");
$ships = $stmt_ships->fetchAll();
$manufacture_lookup = [];
$ships_by_manufacture = [];
$ships_by_id = [];
$ship_lookup = [];
foreach ($manufactures as $manufacture) {
$manufacture_lookup[normalize_catalog_label($manufacture['cl_scmanufactures_name'])] = (int)$manufacture['cl_scmanufactures_id'];
$ships_by_manufacture[(int)$manufacture['cl_scmanufactures_id']] = [];
}
foreach ($ships as $ship) {
$manufactureId = (int)$ship['cl_scvaisseaux_manufacture_id'];
$shipId = (int)$ship['cl_scvaisseaux_id'];
$shipName = $ship['cl_scvaisseaux_name'];
$manufacturerName = $ship['cl_scmanufactures_name'];
$ships_by_manufacture[$manufactureId][] = [
'id' => $shipId,
'name' => $shipName,
];
$ships_by_id[$shipId] = [
'id' => $shipId,
'name' => $shipName,
'manufacture_id' => $manufactureId,
'manufacturer_name' => $manufacturerName,
];
$ship_lookup[$manufactureId . '|' . normalize_catalog_label($shipName)] = $shipId;
}
$stmt_list = $db->query("SELECT
p.*,
m.cl_scmanufactures_name AS relation_manufacturer_name,
v.cl_scvaisseaux_name AS relation_ship_name,
v.cl_scvaisseaux_manufacture_id AS relation_ship_manufacture_id
FROM tbl_scpreset p
LEFT JOIN tbl_scmanufactures m ON m.cl_scmanufactures_id = p.cl_scpreset_manufacture_id
LEFT JOIN tbl_scvaisseaux v ON v.cl_scvaisseaux_id = p.cl_scpreset_vaisseau_id
ORDER BY COALESCE(m.cl_scmanufactures_name, p.cl_scpreset_manufacturer) ASC,
COALESCE(v.cl_scvaisseaux_name, p.cl_scpreset_name) ASC");
$presets = $stmt_list->fetchAll();
foreach ($presets as &$preset) {
$resolvedManufactureId = (int)($preset['cl_scpreset_manufacture_id'] ?? 0);
if ($resolvedManufactureId <= 0) {
$manufacturerKey = normalize_catalog_label((string)($preset['cl_scpreset_manufacturer'] ?? ''));
if ($manufacturerKey !== '' && isset($manufacture_lookup[$manufacturerKey])) {
$resolvedManufactureId = $manufacture_lookup[$manufacturerKey];
}
}
$resolvedShipId = (int)($preset['cl_scpreset_vaisseau_id'] ?? 0);
if ($resolvedShipId <= 0 && $resolvedManufactureId > 0) {
$shipKey = $resolvedManufactureId . '|' . normalize_catalog_label((string)($preset['cl_scpreset_name'] ?? ''));
if (isset($ship_lookup[$shipKey])) {
$resolvedShipId = $ship_lookup[$shipKey];
}
}
$displayManufacturer = $preset['relation_manufacturer_name'] ?: $preset['cl_scpreset_manufacturer'];
$displayName = $preset['relation_ship_name'] ?: $preset['cl_scpreset_name'];
if ($resolvedShipId > 0 && isset($ships_by_id[$resolvedShipId])) {
$displayName = $ships_by_id[$resolvedShipId]['name'];
$displayManufacturer = $ships_by_id[$resolvedShipId]['manufacturer_name'];
$resolvedManufactureId = $ships_by_id[$resolvedShipId]['manufacture_id'];
}
$preset['resolved_manufacture_id'] = $resolvedManufactureId;
$preset['resolved_ship_id'] = $resolvedShipId;
$preset['display_manufacturer'] = $displayManufacturer;
$preset['display_name'] = $displayName;
}
unset($preset);
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Presets Vaisseaux | R.E.A.C.T. Admin</title>
<link rel="stylesheet" type="text/css" href="css/styles.css">
<link rel="stylesheet" type="text/css" href="css/default.css">
<style>
:root {
--primary: #a29b78;
--primary-glow: rgba(162, 155, 120, 0.4);
--bg-dark: #080a0f;
--card-bg: rgba(20, 24, 33, 0.85);
--border-glow: rgba(162, 155, 120, 0.25);
--danger: #ff4d4d;
--success: #00ff88;
}
@font-face {
font-family: 'Electrolize';
src: url('fonts/Electrolize-Regular.ttf') format('truetype');
}
body {
background: radial-gradient(circle at top right, #1a1f2e, var(--bg-dark));
background-attachment: fixed;
color: #e0e0e0;
font-family: 'Electrolize', sans-serif;
margin: 0;
overflow-x: hidden;
min-height: 100vh;
}
.admin-layout {
display: flex;
flex-direction: column;
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
animation: fadeIn 0.6s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.admin-topbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 2rem;
background: var(--card-bg);
backdrop-filter: blur(10px);
border: 1px solid var(--border-glow);
border-radius: 12px;
margin-bottom: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.topbar-info h1 {
margin: 0;
font-size: 1.5rem;
letter-spacing: 2px;
text-transform: uppercase;
background: linear-gradient(90deg, #fff, var(--primary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.topbar-info p {
margin: 0.25rem 0 0;
font-size: 0.85rem;
color: var(--primary);
opacity: 0.8;
}
.topbar-actions {
display: flex;
gap: 1rem;
flex-wrap: wrap;
align-items: center;
}
.session-user {
opacity: 0.85;
}
.btn-modern {
padding: 0.6rem 1.2rem;
border: 1px solid var(--primary);
background: transparent;
color: #fff;
font-family: 'Electrolize', sans-serif;
font-size: 0.9rem;
text-transform: uppercase;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 4px;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 5px;
}
.btn-modern:hover {
background: var(--primary);
color: var(--bg-dark);
box-shadow: 0 0 15px var(--primary-glow);
}
.btn-modern.danger { border-color: var(--danger); color: var(--danger); }
.btn-modern.danger:hover { background: var(--danger); color: #fff; }
.btn-mini { padding: 0.3rem 0.6rem; font-size: 0.75rem; }
.nav-tabs { display: flex; gap: 1rem; margin-bottom: 2rem; border-bottom: 1px solid var(--border-glow); padding-bottom: 1rem; }
.nav-tabs a { text-decoration: none; color: #888; text-transform: uppercase; font-size: 0.9rem; transition: color 0.3s; }
.nav-tabs a:hover, .nav-tabs a.active { color: var(--primary); }
.admin-grid {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 2rem;
}
@media (max-width: 1024px) {
.admin-grid { grid-template-columns: 1fr; }
}
.glass-card {
background: var(--card-bg);
backdrop-filter: blur(12px);
border: 1px solid var(--border-glow);
border-radius: 12px;
padding: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
height: fit-content;
}
.glass-card h2 {
margin-top: 0;
margin-bottom: 1.5rem;
font-size: 1.25rem;
color: var(--primary);
border-bottom: 1px solid var(--border-glow);
padding-bottom: 0.75rem;
}
.form-group { margin-bottom: 1.5rem; }
.form-group label { display: block; margin-bottom: 0.5rem; font-size: 0.85rem; color: #aaa; text-transform: uppercase; }
.form-control {
width: 100%;
padding: 0.8rem 1rem;
background: rgba(0, 0, 0, 0.3);
border: 1px solid #444;
border-radius: 4px;
color: #fff;
font-family: 'Electrolize', sans-serif;
transition: border-color 0.3s;
}
.form-control:focus { outline: none; border-color: var(--primary); background: rgba(0, 0, 0, 0.5); }
select.form-control { background: #353b45; color: #fff; border-color: #565d68; color-scheme: dark; }
select.form-control:focus { background: #3d444f; color: #fff; }
select.form-control option { background: #353b45; color: #fff; }
select.form-control option:checked { background: #4a5260; color: #fff; }
.form-control:disabled { opacity: 0.55; cursor: not-allowed; }
.form-help {
margin-top: 0.5rem;
font-size: 0.78rem;
color: #8f8f8f;
line-height: 1.45;
}
.modern-table { width: 100%; border-collapse: separate; border-spacing: 0 8px; }
.modern-table th { text-align: left; padding: 1rem; font-size: 0.8rem; text-transform: uppercase; color: var(--primary); opacity: 0.7; }
.modern-table td { padding: 1rem; background: rgba(255, 255, 255, 0.03); border-top: 1px solid rgba(255, 255, 255, 0.05); border-bottom: 1px solid rgba(255, 255, 255, 0.05); }
.modern-table td:first-child { border-left: 1px solid rgba(255, 255, 255, 0.05); border-radius: 8px 0 0 8px; }
.modern-table td:last-child { border-right: 1px solid rgba(255, 255, 255, 0.05); border-radius: 0 8px 8px 0; }
.modern-table tr:hover td { background: rgba(162, 155, 120, 0.05); }
.flash { padding: 1rem 1.5rem; border-radius: 8px; margin-bottom: 1.5rem; font-size: 0.9rem; border-left: 4px solid var(--primary); background: rgba(162, 155, 120, 0.1); }
.flash.error { border-color: var(--danger); background: rgba(255, 77, 77, 0.1); color: #ffbaba; }
.flash.success { border-color: var(--success); background: rgba(0, 255, 136, 0.1); color: #baffda; }
.manufacturer-text {
font-size: 0.65rem;
color: #888;
text-transform: uppercase;
letter-spacing: 1px;
display: block;
margin-top: 2px;
}
.creator-text {
font-size: 0.65rem;
color: #888;
font-style: italic;
display: block;
margin-top: 1px;
}
.desc-text {
font-size: 0.8rem;
color: #aaa;
max-width: 300px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.link-text {
font-size: 0.75rem;
color: var(--primary);
opacity: 0.8;
text-decoration: none;
}
.link-text:hover { text-decoration: underline; }
</style>
</head>
<body>
<?php echo auth_render_page_access_widget('scpreset.php', 'Presets Vaisseau'); ?>
<div class="admin-layout">
<header class="admin-topbar">
<div class="topbar-info">
<h1>R.E.A.C.T. Ship Presets</h1>
<p>Niveau d\'accès : <strong><?php echo htmlspecialchars($role_label); ?></strong></p>
</div>
<div class="topbar-actions">
<span class="session-user">Connecté : <strong><?php echo htmlspecialchars($current_session_user); ?></strong></span>
<a href="index.php" class="btn-modern">Retour au site</a>
<a href="logout.php" class="btn-modern danger">Déconnexion</a>
</div>
</header>
<?php echo auth_render_app_nav('scpreset.php'); ?>
<?php if ($flash_message !== ''): ?>
<div class="flash <?php echo htmlspecialchars($flash_type); ?>">
<?php echo htmlspecialchars($flash_message); ?>
</div>
<?php endif; ?>
<div class="admin-grid">
<!-- Left Column: Add/Edit Preset -->
<div class="side-panel">
<section class="glass-card">
<h2 id="formTitle">Nouveau Preset</h2>
<?php if (empty($manufactures)): ?>
<p style="color: var(--danger); font-size: 0.9rem;">Aucune manufacture n'est disponible. Ajoute d'abord une manufacture.</p>
<a href="scmanufactures.php" class="btn-modern" style="width: 100%;">Gérer les manufactures</a>
<?php elseif (empty($ships)): ?>
<p style="color: var(--danger); font-size: 0.9rem;">Aucun vaisseau n'est disponible. Ajoute d'abord au moins un vaisseau lié à une manufacture.</p>
<a href="scvaisseaux.php" class="btn-modern" style="width: 100%;">Gérer les vaisseaux</a>
<?php else: ?>
<form id="presetForm" method="post">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>">
<input type="hidden" name="action" id="formAction" value="add_preset">
<input type="hidden" name="preset_id" id="presetId" value="">
<div class="form-group">
<label>Manufacture</label>
<select name="manufacture_id" id="presetManufactureId" class="form-control" required>
<option value="">- Sélectionner une manufacture -</option>
<?php foreach ($manufactures as $m): ?>
<option value="<?php echo $m['cl_scmanufactures_id']; ?>"><?php echo htmlspecialchars($m['cl_scmanufactures_name']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label>Vaisseau</label>
<select name="ship_id" id="presetShipId" class="form-control" required disabled>
<option value="">- Choisissez d'abord une manufacture -</option>
</select>
<div class="form-help">Le vaisseau affiché dépend de la manufacture choisie. Le nom et la manufacture du preset seront remplis automatiquement.</div>
</div>
<div class="form-group">
<label>Description</label>
<textarea name="description" id="presetDescription" class="form-control" rows="3" placeholder="Description du preset..."></textarea>
</div>
<div class="form-group">
<label>Lien Externe</label>
<input type="url" name="link" id="presetLink" class="form-control" required placeholder="https://...">
</div>
<button type="submit" id="submitBtn" class="btn-modern" style="width: 100%;">Ajouter</button>
<button type="button" id="cancelBtn" class="btn-modern" style="width: 100%; margin-top: 10px; display: none;" onclick="resetForm()">Annuler</button>
</form>
<?php endif; ?>
</section>
</div>
<!-- Right Column: List -->
<main class="main-panel">
<section class="glass-card">
<h2>Liste des Presets</h2>
<div style="overflow-x: auto;">
<table class="modern-table">
<thead>
<tr>
<th>Vaisseau / Manufacture</th>
<th>Description</th>
<th>Lien</th>
<th style="text-align: right;">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($presets)): ?>
<tr><td colspan="4" style="text-align: center; padding: 3rem; color: #666;">Aucun preset enregistré.</td></tr>
<?php else: ?>
<?php foreach ($presets as $p): ?>
<tr>
<td>
<strong style="color: var(--primary); text-transform: uppercase;"><?php echo htmlspecialchars($p['display_name']); ?></strong><br>
<span class="manufacturer-text"><?php echo htmlspecialchars($p['display_manufacturer']); ?></span>
<span class="creator-text">Par <?php echo htmlspecialchars($p['cl_scpreset_creator'] ?: 'Inconnu'); ?></span>
</td>
<td>
<div class="desc-text" title="<?php echo htmlspecialchars($p['cl_scpreset_description']); ?>">
<?php echo htmlspecialchars($p['cl_scpreset_description'] ?: 'Aucune description'); ?>
</div>
</td>
<td>
<a href="<?php echo htmlspecialchars($p['cl_scpreset_link']); ?>" target="_blank" class="link-text">Consulter le lien</a>
</td>
<td style="text-align: right;">
<div style="display: flex; gap: 5px; justify-content: flex-end;">
<button type="button" class="btn-modern btn-mini"
onclick='editPreset(<?php echo json_encode([
"id" => $p["cl_scpreset_id"],
"manufacture_id" => $p["resolved_manufacture_id"],
"ship_id" => $p["resolved_ship_id"],
"name" => $p["display_name"],
"manufacturer" => $p["display_manufacturer"],
"description" => $p["cl_scpreset_description"],
"link" => $p["cl_scpreset_link"]
], JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>)'>
Edit
</button>
<form method="post" onsubmit="return confirm('Supprimer ce preset ?');">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>">
<input type="hidden" name="action" value="delete_preset">
<input type="hidden" name="preset_id" value="<?php echo $p['cl_scpreset_id']; ?>">
<button type="submit" class="btn-modern btn-mini danger">X</button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</section>
</main>
</div>
</div>
<script>
const shipsByManufacture = <?php echo json_encode($ships_by_manufacture, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>;
const presetForm = document.getElementById('presetForm');
const manufactureSelect = document.getElementById('presetManufactureId');
const shipSelect = document.getElementById('presetShipId');
function populateShipOptions(manufactureId, selectedShipId = '') {
if (!shipSelect) {
return;
}
shipSelect.innerHTML = '';
if (!manufactureId || !shipsByManufacture[manufactureId] || shipsByManufacture[manufactureId].length === 0) {
shipSelect.disabled = true;
shipSelect.innerHTML = '<option value="">- Choisissez d\'abord une manufacture -</option>';
return;
}
shipSelect.disabled = false;
shipSelect.innerHTML = '<option value="">- Sélectionner un vaisseau -</option>';
shipsByManufacture[manufactureId].forEach((ship) => {
const option = document.createElement('option');
option.value = String(ship.id);
option.textContent = ship.name;
if (selectedShipId && String(selectedShipId) === String(ship.id)) {
option.selected = true;
}
shipSelect.appendChild(option);
});
}
if (manufactureSelect) {
manufactureSelect.addEventListener('change', function () {
populateShipOptions(this.value, '');
});
populateShipOptions(manufactureSelect.value, shipSelect ? shipSelect.value : '');
}
function editPreset(data) {
if (!presetForm || !manufactureSelect || !shipSelect) {
return;
}
document.getElementById('formAction').value = 'update_preset';
document.getElementById('presetId').value = data.id;
manufactureSelect.value = data.manufacture_id ? String(data.manufacture_id) : '';
populateShipOptions(manufactureSelect.value, data.ship_id ? String(data.ship_id) : '');
document.getElementById('presetDescription').value = data.description;
document.getElementById('presetLink').value = data.link;
document.getElementById('submitBtn').innerText = 'Mettre à jour';
document.getElementById('cancelBtn').style.display = 'block';
document.getElementById('formTitle').innerText = data.name && data.manufacturer
? `Modifier le Preset · ${data.manufacturer} / ${data.name}`
: 'Modifier le Preset';
presetForm.scrollIntoView({ behavior: 'smooth' });
}
function resetForm() {
if (!presetForm) {
return;
}
document.getElementById('formAction').value = 'add_preset';
document.getElementById('presetId').value = '';
presetForm.reset();
populateShipOptions('', '');
document.getElementById('submitBtn').innerText = 'Ajouter';
document.getElementById('cancelBtn').style.display = 'none';
document.getElementById('formTitle').innerText = 'Nouveau Preset';
}
</script>
</body>
</html>

517
scstatsitem.php Normal file
View File

@ -0,0 +1,517 @@
<?php
require_once __DIR__ . '/db/auth.php';
require_once __DIR__ . '/db/scstatsitem.php';
auth_start_session();
auth_bootstrap();
auth_handle_page_access_post('scstatsitem.php', 'Stats Item');
auth_require_page_access('scstatsitem.php', 'Stats Item');
scstatsitem_bootstrap();
$flash = auth_flash_get();
$flash_type = $flash['type'] ?? '';
$flash_message = $flash['message'] ?? '';
$db = db();
$csrf_token = auth_csrf_token();
$allowed_units = ['', '%', '°C', 'RPM', 'Q', 'SCU', 'mRem', 'mRem/s'];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$submitted_csrf = $_POST['csrf_token'] ?? '';
if (!auth_validate_csrf($submitted_csrf)) {
auth_flash_set('error', 'Jeton CSRF invalide.');
header('Location: scstatsitem.php');
exit;
}
$action = $_POST['action'] ?? '';
if ($action === 'add_stat') {
$name = trim($_POST['name'] ?? '');
$unit = trim($_POST['unit'] ?? '%');
if (!in_array($unit, $allowed_units, true)) {
$unit = '%';
}
if ($name === '') {
auth_flash_set('error', 'Le nom de la statistique est requis.');
} else {
try {
$stmt = $db->prepare('INSERT INTO tbl_scstatsitem (cl_scstatsitem_name, cl_scstatsitem_unit) VALUES (:name, :unit)');
$stmt->execute([
'name' => $name,
'unit' => $unit,
]);
auth_flash_set('success', 'Statistique ajoutée avec succès.');
} catch (PDOException $e) {
if ($e->getCode() == 23000) {
auth_flash_set('error', 'Cette statistique existe déjà.');
} else {
auth_flash_set('error', 'Erreur lors de l\'ajout : ' . $e->getMessage());
}
}
}
header('Location: scstatsitem.php');
exit;
}
if ($action === 'update_stat') {
$id = (int) ($_POST['stat_id'] ?? 0);
$name = trim($_POST['name'] ?? '');
$unit = trim($_POST['unit'] ?? '%');
if (!in_array($unit, $allowed_units, true)) {
$unit = '%';
}
if ($id <= 0 || $name === '') {
auth_flash_set('error', 'Données invalides.');
} else {
try {
$stmt = $db->prepare('UPDATE tbl_scstatsitem SET cl_scstatsitem_name = :name, cl_scstatsitem_unit = :unit WHERE cl_scstatsitem_id = :id');
$stmt->execute([
'name' => $name,
'unit' => $unit,
'id' => $id,
]);
auth_flash_set('success', 'Statistique mise à jour.');
} catch (PDOException $e) {
if ($e->getCode() == 23000) {
auth_flash_set('error', 'Cette statistique existe déjà.');
} else {
auth_flash_set('error', 'Erreur lors de la mise à jour : ' . $e->getMessage());
}
}
}
header('Location: scstatsitem.php');
exit;
}
if ($action === 'delete_stat') {
$id = (int) ($_POST['stat_id'] ?? 0);
if ($id > 0) {
try {
$stmt = $db->prepare('DELETE FROM tbl_scstatsitem WHERE cl_scstatsitem_id = :id');
$stmt->execute(['id' => $id]);
auth_flash_set('success', 'Statistique supprimée.');
} catch (PDOException $e) {
auth_flash_set('error', 'Erreur lors de la suppression : ' . $e->getMessage());
}
}
header('Location: scstatsitem.php');
exit;
}
}
$stmt_stats = $db->query('SELECT * FROM tbl_scstatsitem ORDER BY cl_scstatsitem_name ASC, cl_scstatsitem_id ASC');
$stats_items = $stmt_stats->fetchAll();
$current_session_user = $_SESSION['user'] ?? '';
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Stats Item | R.E.A.C.T. Admin</title>
<link rel="stylesheet" type="text/css" href="css/styles.css">
<link rel="stylesheet" type="text/css" href="css/default.css">
<style>
:root {
--primary: #a29b78;
--primary-glow: rgba(162, 155, 120, 0.4);
--bg-dark: #080a0f;
--card-bg: rgba(20, 24, 33, 0.85);
--border-glow: rgba(162, 155, 120, 0.25);
--danger: #ff4d4d;
--success: #00ff88;
}
@font-face {
font-family: 'Electrolize';
src: url('fonts/Electrolize-Regular.ttf') format('truetype');
}
body {
background: radial-gradient(circle at top right, #1a1f2e, var(--bg-dark));
background-attachment: fixed;
color: #e0e0e0;
font-family: 'Electrolize', sans-serif;
margin: 0;
overflow-x: hidden;
min-height: 100vh;
}
.admin-layout {
display: flex;
flex-direction: column;
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
animation: fadeIn 0.6s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.admin-topbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 2rem;
background: var(--card-bg);
backdrop-filter: blur(10px);
border: 1px solid var(--border-glow);
border-radius: 12px;
margin-bottom: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.topbar-info h1 {
margin: 0;
font-size: 1.5rem;
letter-spacing: 2px;
text-transform: uppercase;
background: linear-gradient(90deg, #fff, var(--primary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.topbar-info p {
margin: 0.25rem 0 0;
font-size: 0.85rem;
color: var(--primary);
opacity: 0.8;
}
.topbar-actions {
display: flex;
gap: 1rem;
flex-wrap: wrap;
align-items: center;
}
.session-user {
opacity: 0.85;
}
.btn-modern {
padding: 0.6rem 1.2rem;
border: 1px solid var(--primary);
background: transparent;
color: #fff;
font-family: 'Electrolize', sans-serif;
font-size: 0.9rem;
text-transform: uppercase;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 4px;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 5px;
}
.btn-modern:hover {
background: var(--primary);
color: var(--bg-dark);
box-shadow: 0 0 15px var(--primary-glow);
}
.btn-modern.danger { border-color: var(--danger); color: var(--danger); }
.btn-modern.danger:hover { background: var(--danger); color: #fff; }
.btn-mini { padding: 0.3rem 0.6rem; font-size: 0.75rem; }
.nav-tabs { display: flex; gap: 1rem; margin-bottom: 2rem; border-bottom: 1px solid var(--border-glow); padding-bottom: 1rem; }
.nav-tabs a { text-decoration: none; color: #888; text-transform: uppercase; font-size: 0.9rem; transition: color 0.3s; }
.nav-tabs a:hover, .nav-tabs a.active { color: var(--primary); }
.admin-grid {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 2rem;
}
.side-panel, .main-panel { display: flex; flex-direction: column; gap: 2rem; }
.glass-card {
background: var(--card-bg);
border: 1px solid var(--border-glow);
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(10px);
}
.glass-card h2 {
margin: 0 0 1.25rem;
color: var(--primary);
font-size: 1.1rem;
text-transform: uppercase;
letter-spacing: 1px;
}
.form-group { margin-bottom: 1rem; }
label {
display: block;
margin-bottom: 0.5rem;
font-size: 0.85rem;
color: var(--primary);
text-transform: uppercase;
letter-spacing: 1px;
}
.form-control {
width: 100%;
padding: 0.8rem;
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--border-glow);
border-radius: 6px;
color: #fff;
font-family: 'Electrolize', sans-serif;
box-sizing: border-box;
transition: all 0.3s;
}
.form-control:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px var(--primary-glow);
}
select.form-control {
background: #353b45;
color: #fff;
border-color: #565d68;
color-scheme: dark;
}
select.form-control:focus {
background: #3d444f;
color: #fff;
}
select.form-control option {
background: #353b45;
color: #fff;
}
select.form-control option:checked {
background: #4a5260;
color: #fff;
}
.form-help {
margin-top: 0.75rem;
color: #9ca3af;
font-size: 0.85rem;
line-height: 1.5;
}
.flash {
padding: 1rem 1.25rem;
border-radius: 10px;
margin-bottom: 1.5rem;
border: 1px solid var(--border-glow);
background: rgba(20, 24, 33, 0.9);
}
.flash.success { border-color: rgba(0, 255, 136, 0.35); color: var(--success); }
.flash.error { border-color: rgba(255, 77, 77, 0.35); color: #ff8a8a; }
.modern-table {
width: 100%;
border-collapse: collapse;
min-width: 520px;
}
.modern-table th,
.modern-table td {
padding: 0.9rem 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
vertical-align: middle;
}
.modern-table th {
text-align: left;
color: var(--primary);
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 1px;
}
.modern-table tr:hover td {
background: rgba(255, 255, 255, 0.02);
}
.empty-state {
text-align: center;
padding: 3rem 1rem;
color: #777;
}
@media (max-width: 980px) {
.admin-grid {
grid-template-columns: 1fr;
}
.admin-topbar {
flex-direction: column;
gap: 1rem;
align-items: flex-start;
}
.nav-tabs {
flex-wrap: wrap;
}
}
</style>
</head>
<body>
<?php echo auth_render_page_access_widget('scstatsitem.php', 'Stats Item'); ?>
<div class="admin-layout">
<header class="admin-topbar">
<div class="topbar-info">
<h1>Stats Item</h1>
<p>Gestion libre des statistiques d'objets</p>
</div>
<div class="topbar-actions">
<span class="session-user">Connecté : <strong><?php echo htmlspecialchars($current_session_user, ENT_QUOTES, 'UTF-8'); ?></strong></span>
<a href="index.php" class="btn-modern">Retour au site</a>
<a href="logout.php" class="btn-modern danger">Déconnexion</a>
</div>
</header>
<?php echo auth_render_app_nav('scstatsitem.php'); ?>
<?php if ($flash_message !== ''): ?>
<div class="flash <?php echo htmlspecialchars($flash_type, ENT_QUOTES, 'UTF-8'); ?>">
<?php echo htmlspecialchars($flash_message, ENT_QUOTES, 'UTF-8'); ?>
</div>
<?php endif; ?>
<div class="admin-grid">
<div class="side-panel">
<section class="glass-card">
<h2 id="formTitle">Nouvelle Stat Item</h2>
<form id="statsItemForm" method="post">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="action" id="formAction" value="add_stat">
<input type="hidden" name="stat_id" id="statId" value="">
<div class="form-group">
<label for="statName">Nom de la statistique</label>
<input type="text" name="name" id="statName" class="form-control" required placeholder="Ex : Puissance, Résistance, Vitesse...">
<div class="form-help">Ajoute autant de stats que tu veux. Chaque ligne représente une statistique personnalisée que tu pourras gérer librement.</div>
</div>
<div class="form-group">
<label for="statUnit">Unité de la statistique</label>
<select name="unit" id="statUnit" class="form-control">
<?php foreach ($allowed_units as $unit_option): ?>
<option value="<?php echo htmlspecialchars($unit_option, ENT_QUOTES, 'UTF-8'); ?>" <?php echo $unit_option === '%' ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($unit_option, ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" id="submitBtn" class="btn-modern" style="width: 100%;">Ajouter</button>
<button type="button" id="cancelBtn" class="btn-modern" style="width: 100%; margin-top: 10px; display: none;" onclick="resetForm()">Annuler</button>
</form>
</section>
</div>
<main class="main-panel">
<section class="glass-card">
<h2>Liste des Stats Item</h2>
<div style="overflow-x: auto;">
<table class="modern-table">
<thead>
<tr>
<th>ID</th>
<th>Nom</th>
<th>Unité</th>
<th style="text-align: right;">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($stats_items)): ?>
<tr>
<td colspan="4" class="empty-state">Aucune statistique enregistrée.</td>
</tr>
<?php else: ?>
<?php foreach ($stats_items as $stat): ?>
<tr>
<td style="width: 70px; opacity: 0.5;">#<?php echo (int) $stat['cl_scstatsitem_id']; ?></td>
<td>
<strong style="color: var(--primary); text-transform: uppercase;">
<?php echo htmlspecialchars($stat['cl_scstatsitem_name'], ENT_QUOTES, 'UTF-8'); ?>
</strong>
</td>
<td><?php echo htmlspecialchars($stat['cl_scstatsitem_unit'], ENT_QUOTES, 'UTF-8'); ?></td>
<td style="text-align: right;">
<div style="display: flex; gap: 5px; justify-content: flex-end;">
<button
type="button"
class="btn-modern btn-mini"
onclick='editStatItem(<?php echo json_encode([
"id" => (int) $stat["cl_scstatsitem_id"],
"name" => $stat["cl_scstatsitem_name"],
"unit" => $stat["cl_scstatsitem_unit"],
], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>)'>
Edit
</button>
<form method="post" onsubmit="return confirm('Supprimer cette statistique ?');">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="action" value="delete_stat">
<input type="hidden" name="stat_id" value="<?php echo (int) $stat['cl_scstatsitem_id']; ?>">
<button type="submit" class="btn-modern btn-mini danger">X</button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</section>
</main>
</div>
</div>
<script>
function editStatItem(data) {
document.getElementById('formAction').value = 'update_stat';
document.getElementById('statId').value = data.id;
document.getElementById('statName').value = data.name;
document.getElementById('statUnit').value = data.unit || '%';
document.getElementById('submitBtn').innerText = 'Mettre à jour';
document.getElementById('cancelBtn').style.display = 'block';
document.getElementById('formTitle').innerText = 'Modifier Stat Item';
document.getElementById('statsItemForm').scrollIntoView({ behavior: 'smooth' });
}
function resetForm() {
document.getElementById('formAction').value = 'add_stat';
document.getElementById('statId').value = '';
document.getElementById('statsItemForm').reset();
document.getElementById('submitBtn').innerText = 'Ajouter';
document.getElementById('cancelBtn').style.display = 'none';
document.getElementById('formTitle').innerText = 'Nouvelle Stat Item';
}
</script>
</body>
</html>

488
scvaisseaux.php Normal file
View File

@ -0,0 +1,488 @@
<?php
require_once __DIR__ . '/db/auth.php';
auth_start_session();
auth_bootstrap();
auth_handle_page_access_post('scvaisseaux.php', 'Vaisseaux');
auth_require_page_access('scvaisseaux.php', 'Vaisseaux');
$flash = auth_flash_get();
$flash_type = $flash['type'] ?? '';
$flash_message = $flash['message'] ?? '';
$db = db();
$csrf_token = auth_csrf_token();
// Handle POST actions
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$submitted_csrf = $_POST['csrf_token'] ?? '';
if (!auth_validate_csrf($submitted_csrf)) {
auth_flash_set('error', 'Jeton CSRF invalide.');
header('Location: scvaisseaux.php');
exit;
}
$action = $_POST['action'] ?? '';
// Add ship
if ($action === 'add_ship') {
$name = trim($_POST['name'] ?? '');
$manufacture_id = (int)($_POST['manufacture_id'] ?? 0);
if ($name !== '' && $manufacture_id > 0) {
try {
$stmt = $db->prepare("INSERT INTO tbl_scvaisseaux (cl_scvaisseaux_name, cl_scvaisseaux_manufacture_id) VALUES (:name, :manufacture_id)");
$stmt->execute(['name' => $name, 'manufacture_id' => $manufacture_id]);
auth_flash_set('success', 'Vaisseau ajouté avec succès.');
} catch (PDOException $e) {
auth_flash_set('error', 'Erreur lors de l\'ajout : ' . $e->getMessage());
}
} else {
auth_flash_set('error', 'Veuillez remplir tous les champs obligatoires.');
}
header('Location: scvaisseaux.php');
exit;
}
// Update ship
if ($action === 'update_ship') {
$id = (int)($_POST['ship_id'] ?? 0);
$name = trim($_POST['name'] ?? '');
$manufacture_id = (int)($_POST['manufacture_id'] ?? 0);
if ($id > 0 && $name !== '' && $manufacture_id > 0) {
try {
$stmt = $db->prepare("UPDATE tbl_scvaisseaux SET cl_scvaisseaux_name = :name, cl_scvaisseaux_manufacture_id = :manufacture_id WHERE cl_scvaisseaux_id = :id");
$stmt->execute(['name' => $name, 'manufacture_id' => $manufacture_id, 'id' => $id]);
auth_flash_set('success', 'Vaisseau mis à jour.');
} catch (PDOException $e) {
auth_flash_set('error', 'Erreur lors de la mise à jour : ' . $e->getMessage());
}
} else {
auth_flash_set('error', 'Données invalides.');
}
header('Location: scvaisseaux.php');
exit;
}
// Delete ship
if ($action === 'delete_ship') {
$id = (int)($_POST['ship_id'] ?? 0);
if ($id > 0) {
$stmt = $db->prepare("DELETE FROM tbl_scvaisseaux WHERE cl_scvaisseaux_id = :id");
$stmt->execute(['id' => $id]);
auth_flash_set('success', 'Vaisseau supprimé.');
}
header('Location: scvaisseaux.php');
exit;
}
}
// Fetch all manufactures for the dropdown
$stmt_mans = $db->query("SELECT * FROM tbl_scmanufactures ORDER BY cl_scmanufactures_name ASC");
$manufactures = $stmt_mans->fetchAll();
// Fetch all ships with manufacture names
$stmt_list = $db->query("SELECT v.*, m.cl_scmanufactures_name
FROM tbl_scvaisseaux v
JOIN tbl_scmanufactures m ON v.cl_scvaisseaux_manufacture_id = m.cl_scmanufactures_id
ORDER BY m.cl_scmanufactures_name ASC, v.cl_scvaisseaux_name ASC");
$ships = $stmt_list->fetchAll();
$current_session_user = $_SESSION['user'] ?? '';
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vaisseaux | R.E.A.C.T. Admin</title>
<link rel="stylesheet" type="text/css" href="css/styles.css">
<link rel="stylesheet" type="text/css" href="css/default.css">
<style>
:root {
--primary: #a29b78;
--primary-glow: rgba(162, 155, 120, 0.4);
--bg-dark: #080a0f;
--card-bg: rgba(20, 24, 33, 0.85);
--border-glow: rgba(162, 155, 120, 0.25);
--danger: #ff4d4d;
--success: #00ff88;
}
@font-face {
font-family: 'Electrolize';
src: url('fonts/Electrolize-Regular.ttf') format('truetype');
}
body {
background: radial-gradient(circle at top right, #1a1f2e, var(--bg-dark));
background-attachment: fixed;
color: #e0e0e0;
font-family: 'Electrolize', sans-serif;
margin: 0;
overflow-x: hidden;
min-height: 100vh;
}
.admin-layout {
display: flex;
flex-direction: column;
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
animation: fadeIn 0.6s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.admin-topbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 2rem;
background: var(--card-bg);
backdrop-filter: blur(10px);
border: 1px solid var(--border-glow);
border-radius: 12px;
margin-bottom: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.topbar-info h1 {
margin: 0;
font-size: 1.5rem;
letter-spacing: 2px;
text-transform: uppercase;
background: linear-gradient(90deg, #fff, var(--primary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.topbar-info p {
margin: 0.25rem 0 0;
font-size: 0.85rem;
color: var(--primary);
opacity: 0.8;
}
.topbar-actions {
display: flex;
gap: 1rem;
flex-wrap: wrap;
align-items: center;
}
.session-user {
opacity: 0.85;
}
.btn-modern {
padding: 0.6rem 1.2rem;
border: 1px solid var(--primary);
background: transparent;
color: #fff;
font-family: 'Electrolize', sans-serif;
font-size: 0.9rem;
text-transform: uppercase;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 4px;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 5px;
}
.btn-modern:hover {
background: var(--primary);
color: var(--bg-dark);
box-shadow: 0 0 15px var(--primary-glow);
}
.btn-modern.danger { border-color: var(--danger); color: var(--danger); }
.btn-modern.danger:hover { background: var(--danger); color: #fff; }
.btn-mini { padding: 0.3rem 0.6rem; font-size: 0.75rem; }
.nav-tabs { display: flex; gap: 1rem; margin-bottom: 2rem; border-bottom: 1px solid var(--border-glow); padding-bottom: 1rem; }
.nav-tabs a { text-decoration: none; color: #888; text-transform: uppercase; font-size: 0.9rem; transition: color 0.3s; }
.nav-tabs a:hover, .nav-tabs a.active { color: var(--primary); }
.admin-grid {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 2rem;
}
@media (max-width: 1024px) {
.admin-grid { grid-template-columns: 1fr; }
}
.glass-card {
background: var(--card-bg);
backdrop-filter: blur(12px);
border: 1px solid var(--border-glow);
border-radius: 12px;
padding: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
height: fit-content;
}
.glass-card h2 {
margin-top: 0;
margin-bottom: 1.5rem;
font-size: 1.25rem;
color: var(--primary);
border-bottom: 1px solid var(--border-glow);
padding-bottom: 0.75rem;
}
.form-group { margin-bottom: 1.5rem; }
.form-group label { display: block; margin-bottom: 0.5rem; font-size: 0.85rem; color: #aaa; text-transform: uppercase; }
.form-control {
width: 100%;
padding: 0.8rem 1rem;
background: rgba(0, 0, 0, 0.3);
border: 1px solid #444;
border-radius: 4px;
color: #fff;
font-family: 'Electrolize', sans-serif;
transition: border-color 0.3s;
}
.form-control:focus { outline: none; border-color: var(--primary); background: rgba(0, 0, 0, 0.5); }
select.form-control { background: #353b45; color: #fff; border-color: #565d68; color-scheme: dark; }
select.form-control:focus { background: #3d444f; color: #fff; }
select.form-control option { background: #353b45; color: #fff; }
select.form-control option:checked { background: #4a5260; color: #fff; }
.modern-table { width: 100%; border-collapse: separate; border-spacing: 0 8px; }
.modern-table th { text-align: left; padding: 1rem; font-size: 0.8rem; text-transform: uppercase; color: var(--primary); opacity: 0.7; }
.modern-table td { padding: 1rem; background: rgba(255, 255, 255, 0.03); border-top: 1px solid rgba(255, 255, 255, 0.05); border-bottom: 1px solid rgba(255, 255, 255, 0.05); }
.modern-table td:first-child { border-left: 1px solid rgba(255, 255, 255, 0.05); border-radius: 8px 0 0 8px; }
.modern-table td:last-child { border-right: 1px solid rgba(255, 255, 255, 0.05); border-radius: 0 8px 8px 0; }
.modern-table tr:hover td { background: rgba(162, 155, 120, 0.05); }
.flash { padding: 1rem 1.5rem; border-radius: 8px; margin-bottom: 1.5rem; font-size: 0.9rem; border-left: 4px solid var(--primary); background: rgba(162, 155, 120, 0.1); }
.flash.error { border-color: var(--danger); background: rgba(255, 77, 77, 0.1); color: #ffbaba; }
.flash.success { border-color: var(--success); background: rgba(0, 255, 136, 0.1); color: #baffda; }
.manufacturer-text {
font-size: 0.65rem;
color: #888;
text-transform: uppercase;
letter-spacing: 1px;
display: block;
margin-top: 2px;
}
.ship-filter-status {
margin: 0 0 1rem;
font-size: 0.9rem;
color: #b8bdc7;
}
.ship-filter-status strong {
color: #fff;
}
.ship-filter-empty {
display: none;
text-align: center;
padding: 1rem;
color: #8d94a0;
font-size: 0.95rem;
}
</style>
</head>
<body>
<?php echo auth_render_page_access_widget('scvaisseaux.php', 'Vaisseaux'); ?>
<div class="admin-layout">
<header class="admin-topbar">
<div class="topbar-info">
<h1>Gestion Vaisseaux</h1>
<p>Niveau d\'accès : <strong>Administrateur</strong></p>
</div>
<div class="topbar-actions">
<span class="session-user">Connecté : <strong><?php echo htmlspecialchars($current_session_user); ?></strong></span>
<a href="index.php" class="btn-modern">Retour au site</a>
<a href="logout.php" class="btn-modern danger">Déconnexion</a>
</div>
</header>
<?php echo auth_render_app_nav('scvaisseaux.php'); ?>
<?php if ($flash_message !== ''): ?>
<div class="flash <?php echo htmlspecialchars($flash_type); ?>">
<?php echo htmlspecialchars($flash_message); ?>
</div>
<?php endif; ?>
<div class="admin-grid">
<!-- Left Column: Add/Edit -->
<div class="side-panel">
<section class="glass-card">
<h2 id="formTitle">Nouveau Vaisseau</h2>
<?php if (empty($manufactures)): ?>
<p style="color: var(--danger); font-size: 0.9rem;">Veuillez d\'abord ajouter au moins une manufacture.</p>
<a href="scmanufactures.php" class="btn-modern" style="width: 100%;">Aller aux Manufactures</a>
<?php else: ?>
<form id="shipForm" method="post">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>">
<input type="hidden" name="action" id="formAction" value="add_ship">
<input type="hidden" name="ship_id" id="shipId" value="">
<div class="form-group">
<label>Manufacture</label>
<select name="manufacture_id" id="shipManufacture" class="form-control" required>
<option value="">- Sélectionner -</option>
<?php foreach ($manufactures as $m): ?>
<option value="<?php echo $m['cl_scmanufactures_id']; ?>"><?php echo htmlspecialchars($m['cl_scmanufactures_name']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label>Nom du Vaisseau</label>
<input type="text" name="name" id="shipName" class="form-control" required placeholder="ex: Carrack">
</div>
<button type="submit" id="submitBtn" class="btn-modern" style="width: 100%;">Ajouter</button>
<button type="button" id="cancelBtn" class="btn-modern" style="width: 100%; margin-top: 10px; display: none;" onclick="resetForm()">Annuler</button>
</form>
<?php endif; ?>
</section>
</div>
<!-- Right Column: List -->
<main class="main-panel">
<section class="glass-card">
<h2>Liste des Vaisseaux</h2>
<p id="shipFilterStatus" class="ship-filter-status">Affichage : <strong>tous les vaisseaux</strong></p>
<div style="overflow-x: auto;">
<table class="modern-table">
<thead>
<tr>
<th>Manufacture / Modèle</th>
<th style="text-align: right;">Actions</th>
</tr>
</thead>
<tbody id="shipTableBody">
<?php if (empty($ships)): ?>
<tr><td colspan="2" style="text-align: center; padding: 3rem; color: #666;">Aucun vaisseau enregistré.</td></tr>
<?php else: ?>
<?php foreach ($ships as $s): ?>
<tr class="ship-row" data-manufacture-id="<?php echo (int)$s['cl_scvaisseaux_manufacture_id']; ?>" data-manufacture-name="<?php echo htmlspecialchars($s['cl_scmanufactures_name'], ENT_QUOTES); ?>">
<td>
<span class="manufacturer-text"><?php echo htmlspecialchars($s['cl_scmanufactures_name']); ?></span>
<strong style="color: var(--primary); text-transform: uppercase; font-size: 1.1rem;"><?php echo htmlspecialchars($s['cl_scvaisseaux_name']); ?></strong>
</td>
<td style="text-align: right;">
<div style="display: flex; gap: 5px; justify-content: flex-end;">
<button type="button" class="btn-modern btn-mini"
onclick='editShip(<?php echo json_encode([
"id" => $s["cl_scvaisseaux_id"],
"name" => $s["cl_scvaisseaux_name"],
"manufacture_id" => $s["cl_scvaisseaux_manufacture_id"]
]); ?>)'>
Edit
</button>
<form method="post" onsubmit="return confirm('Supprimer ce vaisseau ?');">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>">
<input type="hidden" name="action" value="delete_ship">
<input type="hidden" name="ship_id" value="<?php echo $s['cl_scvaisseaux_id']; ?>">
<button type="submit" class="btn-modern btn-mini danger">X</button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
<?php if (!empty($ships)): ?>
<p id="shipFilterEmpty" class="ship-filter-empty">Aucun vaisseau trouvé pour cette manufacture.</p>
<?php endif; ?>
</div>
</section>
</main>
</div>
</div>
<script>
function applyShipManufactureFilter() {
const manufactureSelect = document.getElementById('shipManufacture');
const status = document.getElementById('shipFilterStatus');
const emptyMessage = document.getElementById('shipFilterEmpty');
const rows = document.querySelectorAll('#shipTableBody .ship-row');
if (!manufactureSelect || !status || !rows.length) {
return;
}
const selectedManufactureId = manufactureSelect.value;
const selectedOption = manufactureSelect.options[manufactureSelect.selectedIndex];
const selectedManufactureName = selectedManufactureId && selectedOption
? selectedOption.text.trim()
: '';
let visibleCount = 0;
rows.forEach((row) => {
const matches = !selectedManufactureId || row.dataset.manufactureId === selectedManufactureId;
row.style.display = matches ? '' : 'none';
if (matches) {
visibleCount += 1;
}
});
if (selectedManufactureId && selectedManufactureName) {
const label = visibleCount > 1 ? 'vaisseaux' : 'vaisseau';
status.innerHTML = `Affichage : <strong>${visibleCount} ${label} pour ${selectedManufactureName}</strong>`;
} else {
const label = rows.length > 1 ? 'vaisseaux' : 'vaisseau';
status.innerHTML = `Affichage : <strong>tous les ${rows.length} ${label}</strong>`;
}
if (emptyMessage) {
emptyMessage.style.display = visibleCount === 0 ? 'block' : 'none';
}
}
function editShip(data) {
document.getElementById('formAction').value = 'update_ship';
document.getElementById('shipId').value = data.id;
document.getElementById('shipName').value = data.name;
document.getElementById('shipManufacture').value = data.manufacture_id;
applyShipManufactureFilter();
document.getElementById('submitBtn').innerText = 'Mettre à jour';
document.getElementById('cancelBtn').style.display = 'block';
document.getElementById('formTitle').innerText = 'Modifier Vaisseau';
document.getElementById('shipForm').scrollIntoView({ behavior: 'smooth' });
}
function resetForm() {
document.getElementById('formAction').value = 'add_ship';
document.getElementById('shipId').value = '';
document.getElementById('shipForm').reset();
applyShipManufactureFilter();
document.getElementById('submitBtn').innerText = 'Ajouter';
document.getElementById('cancelBtn').style.display = 'none';
document.getElementById('formTitle').innerText = 'Nouveau Vaisseau';
}
document.addEventListener('DOMContentLoaded', function () {
const manufactureSelect = document.getElementById('shipManufacture');
if (!manufactureSelect) {
return;
}
manufactureSelect.addEventListener('change', applyShipManufactureFilter);
applyShipManufactureFilter();
});
</script>
</body>
</html>

641
scwebhook.php Normal file
View File

@ -0,0 +1,641 @@
<?php
require_once __DIR__ . '/db/auth.php';
require_once __DIR__ . '/db/scdiscord.php';
auth_start_session();
auth_bootstrap();
auth_handle_page_access_post('scwebhook.php', 'WEBHOOK');
auth_require_page_access('scwebhook.php', 'WEBHOOK');
scdiscord_bootstrap();
$db = db();
$csrf_token = auth_csrf_token();
$flash = auth_flash_get();
$flash_type = $flash['type'] ?? '';
$flash_message = $flash['message'] ?? '';
$current_session_user = $_SESSION['user'] ?? '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$submitted_csrf = (string) ($_POST['csrf_token'] ?? '');
if (!auth_validate_csrf($submitted_csrf)) {
auth_flash_set('error', 'Jeton CSRF invalide.');
header('Location: scwebhook.php');
exit;
}
$action = (string) ($_POST['action'] ?? '');
if ($action === 'add_webhook' || $action === 'update_webhook') {
$webhook_id = (int) ($_POST['webhook_id'] ?? 0);
$cl_scwebhook_name = trim((string) ($_POST['cl_scwebhook_name'] ?? ''));
$cl_scwebhook_url = trim((string) ($_POST['cl_scwebhook_url'] ?? ''));
$cl_scwebhook_image_url = trim((string) ($_POST['cl_scwebhook_image_url'] ?? ''));
$cl_scwebhook_border_color = scdiscord_normalize_hex_color((string) ($_POST['cl_scwebhook_border_color'] ?? ''));
$cl_scwebhook_is_forum = 0;
if ($cl_scwebhook_name === '' || $cl_scwebhook_url === '' || $cl_scwebhook_image_url === '') {
auth_flash_set('error', 'Le nom, lURL du webhook et limage sont obligatoires.');
header('Location: scwebhook.php');
exit;
}
if (!filter_var($cl_scwebhook_url, FILTER_VALIDATE_URL)) {
auth_flash_set('error', 'LURL du webhook Discord est invalide.');
header('Location: scwebhook.php');
exit;
}
if (!filter_var($cl_scwebhook_image_url, FILTER_VALIDATE_URL)) {
auth_flash_set('error', 'LURL de limage Discord est invalide.');
header('Location: scwebhook.php');
exit;
}
try {
if ($action === 'add_webhook') {
$stmt = $db->prepare('INSERT INTO tbl_scwebhooks (cl_scwebhook_name, cl_scwebhook_url, cl_scwebhook_image_url, cl_scwebhook_border_color, cl_scwebhook_is_forum) VALUES (:name, :url, :image_url, :border_color, :is_forum)');
$stmt->execute([
'name' => $cl_scwebhook_name,
'url' => $cl_scwebhook_url,
'image_url' => $cl_scwebhook_image_url,
'border_color' => $cl_scwebhook_border_color,
'is_forum' => $cl_scwebhook_is_forum,
]);
auth_flash_set('success', 'Canal Discord ajouté avec succès.');
} else {
if ($webhook_id <= 0) {
throw new RuntimeException('ID de webhook invalide.');
}
$stmt = $db->prepare('UPDATE tbl_scwebhooks SET cl_scwebhook_name = :name, cl_scwebhook_url = :url, cl_scwebhook_image_url = :image_url, cl_scwebhook_border_color = :border_color, cl_scwebhook_is_forum = :is_forum WHERE cl_scwebhook_id = :id');
$stmt->execute([
'name' => $cl_scwebhook_name,
'url' => $cl_scwebhook_url,
'image_url' => $cl_scwebhook_image_url,
'border_color' => $cl_scwebhook_border_color,
'is_forum' => $cl_scwebhook_is_forum,
'id' => $webhook_id,
]);
auth_flash_set('success', 'Canal Discord mis à jour.');
}
} catch (Throwable $e) {
auth_flash_set('error', 'Erreur webhook : ' . $e->getMessage());
}
header('Location: scwebhook.php');
exit;
}
if ($action === 'delete_webhook') {
$webhook_id = (int) ($_POST['webhook_id'] ?? 0);
if ($webhook_id > 0) {
try {
$stmt_usage = $db->prepare('SELECT COUNT(*) FROM tbl_scnotifications WHERE cl_scnotification_webhook_id = :id');
$stmt_usage->execute(['id' => $webhook_id]);
$usage_total = (int) $stmt_usage->fetchColumn();
if ($usage_total > 0) {
auth_flash_set('error', 'Suppression refusée : ce webhook est déjà relié à lhistorique des notifications.');
} else {
$stmt = $db->prepare('DELETE FROM tbl_scwebhooks WHERE cl_scwebhook_id = :id');
$stmt->execute(['id' => $webhook_id]);
auth_flash_set('success', 'Canal Discord supprimé.');
}
} catch (Throwable $e) {
auth_flash_set('error', 'Erreur suppression webhook : ' . $e->getMessage());
}
}
header('Location: scwebhook.php');
exit;
}
}
$stmt_webhooks = $db->query('SELECT * FROM tbl_scwebhooks ORDER BY cl_scwebhook_name ASC');
$webhooks = $stmt_webhooks->fetchAll();
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SC Discord | R.E.A.C.T. Admin</title>
<link rel="stylesheet" type="text/css" href="css/styles.css">
<link rel="stylesheet" type="text/css" href="css/default.css">
<style>
:root {
--primary: #a29b78;
--primary-glow: rgba(162, 155, 120, 0.4);
--bg-dark: #080a0f;
--card-bg: rgba(20, 24, 33, 0.85);
--card-bg-secondary: rgba(255, 255, 255, 0.03);
--border-glow: rgba(162, 155, 120, 0.25);
--danger: #ff4d4d;
--success: #00ff88;
--muted: #aaaaaa;
}
@font-face {
font-family: 'Electrolize';
src: url('fonts/Electrolize-Regular.ttf') format('truetype');
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background: radial-gradient(circle at top right, #1a1f2e, var(--bg-dark));
background-attachment: fixed;
color: #e0e0e0;
font-family: 'Electrolize', sans-serif;
overflow-x: hidden;
}
.admin-layout {
display: flex;
flex-direction: column;
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
animation: fadeIn 0.6s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.admin-topbar,
.glass-card {
background: var(--card-bg);
backdrop-filter: blur(12px);
border: 1px solid var(--border-glow);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.admin-topbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
padding: 1.5rem 2rem;
margin-bottom: 2rem;
}
.topbar-info h1 {
margin: 0;
font-size: 1.5rem;
letter-spacing: 2px;
text-transform: uppercase;
background: linear-gradient(90deg, #fff, var(--primary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.topbar-info p {
margin: 0.25rem 0 0;
font-size: 0.85rem;
color: var(--primary);
opacity: 0.8;
}
.topbar-actions {
display: flex;
gap: 1rem;
flex-wrap: wrap;
align-items: center;
}
.session-user {
opacity: 0.85;
}
.btn-modern {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.45rem;
padding: 0.6rem 1.2rem;
border-radius: 4px;
border: 1px solid var(--primary);
background: transparent;
color: #fff;
text-decoration: none;
text-transform: uppercase;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-family: 'Electrolize', sans-serif;
}
.btn-modern:hover {
background: var(--primary);
color: var(--bg-dark);
box-shadow: 0 0 15px var(--primary-glow);
}
.btn-modern.danger {
border-color: var(--danger);
color: var(--danger);
}
.btn-modern.danger:hover {
background: var(--danger);
color: #fff;
box-shadow: 0 0 15px rgba(255, 77, 77, 0.3);
}
.btn-modern.secondary {
border-color: rgba(255, 255, 255, 0.2);
color: #d6d6d6;
}
.btn-modern.secondary:hover {
background: rgba(255, 255, 255, 0.12);
color: #fff;
box-shadow: none;
}
.btn-mini {
padding: 0.45rem 0.8rem;
font-size: 0.72rem;
}
.nav-tabs {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border-glow);
}
.nav-tabs a {
text-decoration: none;
color: #888;
text-transform: uppercase;
font-size: 0.9rem;
transition: color 0.3s;
}
.nav-tabs a:hover,
.nav-tabs a.active {
color: var(--primary);
}
.flash {
margin-bottom: 1.5rem;
padding: 1rem 1.2rem;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.04);
}
.flash.success { border-color: rgba(0, 255, 136, 0.25); color: #9ff3c8; }
.flash.error { border-color: rgba(255, 77, 77, 0.25); color: #ff9d9d; }
.page-grid {
display: grid;
grid-template-columns: 420px 1fr;
gap: 2rem;
}
.stack {
display: grid;
gap: 1.5rem;
}
.glass-card {
padding: 1.5rem;
}
.glass-card h2 {
margin: 0 0 1.5rem;
padding-bottom: 0.75rem;
font-size: 1.25rem;
color: var(--primary);
border-bottom: 1px solid var(--border-glow);
}
.section-note {
margin: -0.5rem 0 1.2rem;
color: var(--muted);
font-size: 0.82rem;
line-height: 1.5;
}
.form-grid {
display: grid;
gap: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-size: 0.85rem;
color: #aaa;
text-transform: uppercase;
}
.form-control {
width: 100%;
padding: 0.8rem 1rem;
background: rgba(0, 0, 0, 0.3);
border: 1px solid #444;
border-radius: 4px;
color: #fff;
font-family: 'Electrolize', sans-serif;
transition: border-color 0.3s, background 0.3s;
}
.form-control:focus {
outline: none;
border-color: var(--primary);
background: rgba(0, 0, 0, 0.5);
}
.form-control[type="color"] {
min-height: 48px;
padding: 0.35rem;
}
.checkbox-row {
display: flex;
align-items: center;
gap: 0.7rem;
color: #e7e7e7;
font-size: 0.9rem;
}
.list-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
}
.table-wrap {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: separate;
border-spacing: 0 8px;
}
th {
text-align: left;
padding: 1rem 0.75rem;
font-size: 0.8rem;
text-transform: uppercase;
color: var(--primary);
opacity: 0.7;
}
td {
padding: 1rem 0.75rem;
background: var(--card-bg-secondary);
border-top: 1px solid rgba(255, 255, 255, 0.05);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
vertical-align: top;
text-align: left;
font-size: 0.88rem;
}
td:first-child {
border-left: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 8px 0 0 8px;
}
td:last-child {
border-right: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 0 8px 8px 0;
}
tr:hover td {
background: rgba(162, 155, 120, 0.08);
}
.muted {
color: var(--muted);
font-size: 0.8rem;
}
.pill {
display: inline-flex;
align-items: center;
padding: 0.2rem 0.55rem;
border-radius: 999px;
background: rgba(255, 255, 255, 0.07);
font-size: 0.72rem;
color: #fff;
}
.pill.forum {
background: rgba(162, 155, 120, 0.18);
color: #e5dcb7;
}
.banner-preview {
width: 120px;
max-height: 42px;
object-fit: cover;
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.08);
display: block;
background: #17191e;
}
.color-chip {
display: inline-flex;
align-items: center;
gap: 0.45rem;
}
.color-chip::before {
content: '';
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--chip-color, var(--primary));
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.08);
}
.actions-inline {
display: flex;
flex-wrap: wrap;
gap: 0.45rem;
justify-content: flex-end;
}
.empty-state {
padding: 2rem;
border: 1px dashed rgba(255, 255, 255, 0.12);
border-radius: 10px;
color: #9a9a9a;
text-align: center;
}
@media (max-width: 1100px) {
.page-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<?php echo auth_render_page_access_widget('scwebhook.php', 'WEBHOOK'); ?>
<div class="admin-layout">
<header class="admin-topbar">
<div class="topbar-info">
<h1>R.E.A.C.T. SC Webhook</h1>
<p>Niveau d'accès : <strong>Administrateur</strong></p>
</div>
<div class="topbar-actions">
<span class="session-user">Connecté : <strong><?php echo htmlspecialchars($current_session_user, ENT_QUOTES, 'UTF-8'); ?></strong></span>
<a href="index.php" class="btn-modern">Retour au site</a>
<a href="logout.php" class="btn-modern danger">Déconnexion</a>
</div>
</header>
<?php echo auth_render_app_nav('scwebhook.php'); ?>
<?php if ($flash_message !== ''): ?>
<div class="flash <?php echo htmlspecialchars($flash_type, ENT_QUOTES, 'UTF-8'); ?>">
<?php echo htmlspecialchars($flash_message, ENT_QUOTES, 'UTF-8'); ?>
</div>
<?php endif; ?>
<div class="page-grid">
<aside class="stack">
<section class="glass-card">
<h2 id="webhookFormTitle">Nouveau canal Discord</h2>
<p class="section-note">Configure ici toute la logique centralisée du canal Discord : nomination, URL du webhook, image de prévisualisation et couleur de bordure.</p>
<form method="post" id="webhookForm" class="form-grid">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="action" id="webhookAction" value="add_webhook">
<input type="hidden" name="webhook_id" id="webhookId" value="">
<div class="form-group">
<label for="cl_scwebhook_name">Nomination du canal</label>
<input type="text" id="cl_scwebhook_name" name="cl_scwebhook_name" class="form-control" required placeholder="Ex : Code 4">
</div>
<div class="form-group">
<label for="cl_scwebhook_url">Lien du webhook</label>
<input type="url" id="cl_scwebhook_url" name="cl_scwebhook_url" class="form-control" required placeholder="https://discord.com/api/webhooks/...">
</div>
<div class="form-group">
<label for="cl_scwebhook_image_url">Lien image / prévisualisation</label>
<input type="url" id="cl_scwebhook_image_url" name="cl_scwebhook_image_url" class="form-control" required placeholder="https://.../banniere.png">
</div>
<div class="form-group">
<label for="cl_scwebhook_border_color">Couleur de bordure</label>
<input type="color" id="cl_scwebhook_border_color" name="cl_scwebhook_border_color" class="form-control" value="#ffae00">
</div>
<button type="submit" class="btn-modern" id="webhookSubmitBtn">Ajouter le canal</button>
<button type="button" class="btn-modern secondary" id="webhookCancelBtn" style="display:none;" onclick="resetWebhookForm()">Annuler lédition</button>
</form>
</section>
</aside>
<main class="list-grid">
<section class="glass-card">
<h2>Canaux Discord enregistrés</h2>
<?php if (empty($webhooks)): ?>
<div class="empty-state">Aucun canal Discord configuré pour le moment.</div>
<?php else: ?>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Nomination</th>
<th>Webhook</th>
<th>Prévisualisation</th>
<th>Couleur</th>
<th style="text-align:right;">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($webhooks as $webhook): ?>
<tr>
<td>
<strong><?php echo htmlspecialchars($webhook['cl_scwebhook_name'], ENT_QUOTES, 'UTF-8'); ?></strong>
</td>
<td>
<span class="muted"><?php echo htmlspecialchars(scdiscord_mask_webhook_url((string) $webhook['cl_scwebhook_url']), ENT_QUOTES, 'UTF-8'); ?></span>
</td>
<td>
<img class="banner-preview" src="<?php echo htmlspecialchars((string) ($webhook['cl_scwebhook_image_url'] ?? ''), ENT_QUOTES, 'UTF-8'); ?>" alt="Prévisualisation webhook">
</td>
<td>
<?php $webhookBorderColor = scdiscord_normalize_hex_color((string) ($webhook['cl_scwebhook_border_color'] ?? '#ffae00')); ?>
<span class="color-chip" style="--chip-color: <?php echo htmlspecialchars($webhookBorderColor, ENT_QUOTES, 'UTF-8'); ?>;">
<?php echo htmlspecialchars($webhookBorderColor, ENT_QUOTES, 'UTF-8'); ?>
</span>
</td>
<td>
<div class="actions-inline">
<button
type="button"
class="btn-modern btn-mini"
onclick='editWebhook(<?php echo json_encode([
'id' => (int) $webhook['cl_scwebhook_id'],
'name' => (string) $webhook['cl_scwebhook_name'],
'url' => (string) $webhook['cl_scwebhook_url'],
'image_url' => (string) ($webhook['cl_scwebhook_image_url'] ?? ''),
'border_color' => (string) ($webhook['cl_scwebhook_border_color'] ?? '#ffae00'),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>)'
>Éditer</button>
<form method="post" onsubmit="return confirm('Supprimer ce canal Discord ?');">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="action" value="delete_webhook">
<input type="hidden" name="webhook_id" value="<?php echo (int) $webhook['cl_scwebhook_id']; ?>">
<button type="submit" class="btn-modern btn-mini danger">Supprimer</button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</section>
</main>
</div>
</div>
<script>
function editWebhook(webhook) {
document.getElementById('webhookFormTitle').textContent = 'Modifier le canal Discord';
document.getElementById('webhookAction').value = 'update_webhook';
document.getElementById('webhookId').value = webhook.id || '';
document.getElementById('cl_scwebhook_name').value = webhook.name || '';
document.getElementById('cl_scwebhook_url').value = webhook.url || '';
document.getElementById('cl_scwebhook_image_url').value = webhook.image_url || '';
document.getElementById('cl_scwebhook_border_color').value = webhook.border_color || '#ffae00';
document.getElementById('webhookSubmitBtn').textContent = 'Mettre à jour le canal';
document.getElementById('webhookCancelBtn').style.display = 'inline-flex';
}
function resetWebhookForm() {
document.getElementById('webhookFormTitle').textContent = 'Nouveau canal Discord';
document.getElementById('webhookAction').value = 'add_webhook';
document.getElementById('webhookId').value = '';
document.getElementById('cl_scwebhook_name').value = '';
document.getElementById('cl_scwebhook_url').value = '';
document.getElementById('cl_scwebhook_image_url').value = '';
document.getElementById('cl_scwebhook_border_color').value = '#ffae00';
document.getElementById('webhookSubmitBtn').textContent = 'Ajouter le canal';
document.getElementById('webhookCancelBtn').style.display = 'none';
}
</script>
</body>
</html>