Compare commits

...

11 Commits

Author SHA1 Message Date
Flatlogic Bot
cb61082775 Autosave: 20260408-021238 2026-04-08 02:12:38 +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
17 changed files with 2886 additions and 349 deletions

0
.perm_test_apache Normal file
View File

0
.perm_test_exec Normal file
View File

635
admin.php Normal file
View File

@ -0,0 +1,635 @@
<?php
require_once __DIR__ . '/db/auth.php';
auth_start_session();
auth_bootstrap();
if (!auth_is_admin()) {
header('Location: index.php');
exit;
}
$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 ($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, ['admin', 'member'], 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, ['admin', 'member'], 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 ($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;
}
}
$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();
$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;
}
.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);
}
.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-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>Administrateur</strong> | Session : <strong><?php echo htmlspecialchars($current_session_user, ENT_QUOTES, 'UTF-8'); ?></strong></p>
</div>
<div class="topbar-actions">
<a href="index.php" class="btn-modern">Retour au site</a>
<a href="logout.php" class="btn-modern danger">Session End</a>
</div>
</header>
<nav class="nav-tabs">
<a href="admin.php" class="active">Utilisateurs</a>
<a href="scitems.php">Base d'Objets</a>
<a href="scmining.php">Scanner Minage</a>
<a href="scpreset.php">Presets Vaisseau</a>
</nav>
<?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 ($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; ?>
<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' : ''; ?>>Admin</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' : 'badge-member'; ?>">
<?php echo $cl_auth_right; ?>
</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>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 740 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 745 KiB

View File

@ -1,3 +1,4 @@
[hidden] { display: none !important; }
@font-face {
font-family: 'Electrolize';
src: url('../fonts/Electrolize-Regular.ttf') format('truetype');
@ -309,4 +310,49 @@ a:hover {
.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;
}
.connexion-actions[hidden] { display: none !important; }
.connexion-actions {
display: inline-flex;
align-items: center;
gap: 10px;
}
.connexion-actions a {
color: #f4e3b2;
text-decoration: none;
border-bottom: 1px solid rgba(244, 227, 178, 0.35);
}
.connexion-actions a:hover {
color: #ffffff;
border-bottom-color: rgba(255, 255, 255, 0.8);
}
.login-status {
min-height: 18px;
}
.login-status.is-error {
color: #ff8080;
}
.login-status.is-success {
color: #9fe29f;
}

118
db/auth.php Normal file
View File

@ -0,0 +1,118 @@
<?php
require_once __DIR__ . '/config.php';
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', 'member') NOT NULL DEFAULT 'member'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
);
$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();
$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
{
return ['admin', 'ReactAdmin!2026'];
}
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
{
auth_start_session();
return isset($_SESSION['role']) && $_SESSION['role'] === 'admin';
}
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;
}

17
db/config.php Normal file
View File

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

View File

@ -1,172 +1,190 @@
<!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">
<meta name="keywords" content="BlackOps Agency, Star Citizen, Advocacy, UEE, Sécurité, Exploration, Renseignement, Patrouilles, Chasses de primes, Investigation, Neutralisation de menaces, Assistance humanitaire, Abordages, Réseaux anti-toxicité, Formation tactique, Entraînements, Coopération, Zones UEE, Systèmes inexplorés, Lore Star Citizen, Missions multi-joueurs, Protocole Arkange, Protocole Vigilance, Respect des lois impériales, Justice interstellaire, Criminalité galactique, Piraterie, XenoThreat, Vanduul, Sécurité spatiale, Leadership BlackOps, Collaboration stratégique, Combat spatial, Star Citizen gameplay, Exploration stratégique, Missions BlackOps Agency, Réputation UEE, Gestion des conflits, BlackOps, Agency, Recrutement Star Citizen, Zones de guerre, Star Citizen immersion.">
<title>Rapid Emergency & Action Combat Team / Star Citizen</title>
<link rel="stylesheet" type="text/css" href="css/styles.css">
<link rel="stylesheet" type="text/css" href="css/default.css" />
<link rel="stylesheet" type="text/css" href="css/component.css" />
<link rel="stylesheet" type="text/css" href="css/switch.css" />
<link rel="stylesheet" type="text/css" href="css/styles-modal.css" />
<script src="js/modernizr.custom.js"></script>
</head>
<body>
<div class="connexion-div-menu md-trigger" data-modal="modal-Login"><a href="#">Connexion</a></div>
<div class="se-switch switch-center" role="group" aria-label="Choix entre FR et EN">
<a class="se-switch__btn" href="index.php" data-site="se1">🇫🇷</a>
<a class="se-switch__btn is-active" href="index-en.php" data-site="se2" aria-current="page">🇬🇧</a>
</div>
<div class="md-modal md-effect-1" id="modal-Login">
<div class="md-content">
<h3>
<img class="float-left" src="img/icon_bops.png" width="48" height="48" alt="" />
LOGIN INTERFACE
<a class="frame-icon-close md-close float-right" href="#"><img src="img/icon_close.png" width="48" height="48" alt="" /></a>
</h3>
<div>
<p class="txt-center">Enter your credentials to access the secure area.*</p>
<form>
<div>
<p class="txt-center"><input class="connexion-champ" id="idr" type="text" name="username" placeholder="RID" /></p>
<p class="txt-center"><input class="connexion-champ" id="pwr" type="password" name="password" placeholder="PASSWORD" /></p>
</div>
<div>
<p class="txt-center"><input class="connexion-bouton" type="submit" value="Authenticate" /></p>
</div>
</form>
<p class="txt-center txt-s10">* Access under high surveillance. By confirming your credentials, you agree to the R.E.A.C.T. Initiative security protocols. Any breach of protocol is subject to sanctions.</p>
</div>
</div>
</div>
<div class="md-modal md-effect-1" id="modal-About">
<div class="md-content">
<h3>
<img class="float-left" src="img/icon_bops.png" width="48" height="48" alt="" />
MORE INFORMATIONS
<a class="frame-icon-close md-close float-right" href="#"><img src="img/icon_close.png" width="48" height="48" alt="" /></a>
</h3>
<div>
<img class="float-left" src="img/fl_security.png" width="300" height="460" alt="" />
<p>La sécurité et la protection des intérêts impériaux sont des priorités absolues pour lagence.</p>
<p>En tant que garante de lordre dans les zones sous juridiction de lUEE, lagence BOPS s'engage à prévenir, neutraliser et répondre efficacement à toutes les formes de menaces, qu'elles soient internes ou externes aux territoires de l'Empire.</p>
<p>Cette mission repose sur une approche combinant vigilance, réactivité et expertise tactique, afin dassurer la stabilité, la sécurité des citoyens et le respect des lois impériales.</p>
<p>Chaque opération est conçue pour inspirer confiance aux populations locales tout en projetant une image de force et de discipline au sein de lunivers connu.</p>
<p class="ul-style">
Surveillance active des secteurs critiques pour dissuader les menaces potentielles.</br>
Protection rapprochée de convois, personnalités importantes ou cargaisons sensibles.</br>
Neutralisation de vaisseaux hostiles ou suspects, avec récupération dactifs stratégiques.</br>
Ciblage et capture ou élimination de fugitifs dangereux, conformément aux lois impériales.</br>
Protection des civils en détresse et aide aux infrastructures endommagées.</br>
Collecte et analyse dinformations sur des activités suspectes, incidents ou organisations hostiles.
</p>
</div>
</div>
</div>
<div class="center-page-bops"></div>
<div class="center-div-menu">
<div class="padding50">
<p class="txt-center txt-bold txt-s40 small-caps padding25 txt-or">Rapid Emergency & Action Combat Team</p>
<!-- <p class="txt-center txt-bold txt-italic txt-s20 padding15 txt-or"><span class="padding5">SÉCURITÉ</span> —— <span class="padding5">INTELLIGENCE</span> —— <span class="padding5">SAUVETAGE</span></p> -->
<p class="txt-justify padding5">Founded to combat extreme criminality across the Verse. R.E.A.C.T. is an independent operational label, uniting dedicated pilots from all horizons to neutralize threats where the law is failing.</p>
<p class="txt-center txt-bold txt-s22 padding25 txt-or">—————— R.E.A.C.T. is not an organization, but a tactical standard ——————</p>
<p class="txt-justify padding5">It is a cross-org initiative designed for players who share the same vision: protecting citizens and NPCs from hostile entities. Wearing the R.E.A.C.T. badge doesnt mean leaving your organization; it means joining a rapid response network.</p>
</div>
<div class="center-div-menu-flex txt-bold">
<div class="menu-item md-trigger" data-modal="modal-About"><a href="#">> MORE INFORMATIONS <</a></div>
</div>
</div>
<div class="black-filter"></div>
<!-- Vidéo de fond -->
<div class="video-container" aria-hidden="true">
<iframe
id="react-video"
src="https://www.youtube-nocookie.com/embed/tFDqWOqm1G8?controls=0&autoplay=1&mute=1&playsinline=1&loop=1&playlist=tFDqWOqm1G8&cc_load_policy=0&iv_load_policy=3&modestbranding=1&rel=0&enablejsapi=1"
title="Présentation R.E.A.C.T — YouTube"
loading="lazy"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; fullscreen"
allowfullscreen
referrerpolicy="strict-origin-when-cross-origin">
</iframe>
</div>
<div class="md-overlay"></div><!-- the overlay element -->
<!-- classie.js by @desandro: https://github.com/desandro/classie -->
<script src="js/classie.js"></script>
<script src="js/modalEffects.js"></script>
<!-- for the blur effect -->
<!-- by @derSchepp https://github.com/Schepp/CSS-Filters-Polyfill -->
<script>
// this is important for IEs
var polyfilter_scriptpath = '/js/';
</script>
<script src="js/cssParser.js"></script>
<script src="js/css-filters-polyfill.js"></script>
<script>
// On ajoute dynamiquement l'origine à l'URL de l'iframe (recommandé par l'API YouTube)
(function attachOrigin(){
const iframe = document.getElementById('react-video');
const url = new URL(iframe.src);
if (!url.searchParams.has('origin')) {
url.searchParams.set('origin', window.location.origin);
iframe.src = url.toString();
}
})();
// Utilitaires pour parler au lecteur via postMessage (API YouTube)
function ytCommand(func, args = []) {
const iframe = document.getElementById('react-video');
if (!iframe || !iframe.contentWindow) return;
iframe.contentWindow.postMessage(JSON.stringify({ event: 'command', func, args }), '*');
}
// Au premier geste utilisateur : on remet le son et on met le volume à 50 %
function enableSoundOnce() {
ytCommand('unMute');
ytCommand('setVolume', [50]);
// Masquer le bouton
const btn = document.getElementById('unmute-btn');
if (btn) btn.style.display = 'none';
// Retirer les écouteurs pour ne pas répéter laction
window.removeEventListener('pointerdown', enableSoundOnce);
window.removeEventListener('keydown', enableSoundOnce);
window.removeEventListener('touchstart', enableSoundOnce, { passive: true });
}
// Bouton dédié
// document.getElementById('unmute-btn').addEventListener('click', enableSoundOnce);
// …ou nimporte quel premier geste sur la page
//window.addEventListener('pointerdown', enableSoundOnce, { once: true });
//window.addEventListener('keydown', enableSoundOnce, { once: true });
//window.addEventListener('touchstart', enableSoundOnce, { once: true, passive: true });
</script>
<div class="footer txt-s12 txt-center">
<p>
<a href="https://robertsspaceindustries.com/en/orgs/REACT" target="_blank" title="Notre page RSI"><img src="img/icon09b.png" width="48" height="48" alt="" /></a>
<a href="https://discord.gg/dAvST8E7Mq" target="_blank" title="Notre Discord"><img src="img/icon08b.png" width="48" height="48" alt="" /></a>
<a href="https://robertsspaceindustries.com" target="_blank" title="RSI"><img src="img/icon10.png" width="48" height="48" alt="" /></a>
</p>
<p>Copyright © 2025-<?php echo date('Y'); ?> | www.react-sc.fr | All Rights Reserved.</p>
</div>
</body>
</html>
<?php
require_once __DIR__ . '/db/auth.php';
auth_start_session();
auth_bootstrap();
$session_cl_auth_user = isset($_SESSION['user']) ? (string) $_SESSION['user'] : '';
$session_cl_auth_right = isset($_SESSION['role']) ? (string) $_SESSION['role'] : '';
$is_authenticated = $session_cl_auth_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">
<meta name="keywords" content="BlackOps Agency, Star Citizen, Advocacy, UEE, Sécurité, Exploration, Renseignement, Patrouilles, Chasses de primes, Investigation, Neutralisation de menaces, Assistance humanitaire, Abordages, Réseaux anti-toxicité, Formation tactique, Entraînements, Coopération, Zones UEE, Systèmes inexplorés, Lore Star Citizen, Missions multi-joueurs, Protocole Arkange, Protocole Vigilance, Respect des lois impériales, Justice interstellaire, Criminalité galactique, Piraterie, XenoThreat, Vanduul, Sécurité spatiale, Leadership BlackOps, Collaboration stratégique, Combat spatial, Star Citizen gameplay, Exploration stratégique, Missions BlackOps Agency, Réputation UEE, Gestion des conflits, BlackOps, Agency, Recrutement Star Citizen, Zones de guerre, Star Citizen immersion.">
<title>Rapid Emergency & Action Combat Team / Star Citizen</title>
<link rel="stylesheet" type="text/css" href="css/styles.css">
<link rel="stylesheet" type="text/css" href="css/default.css" />
<link rel="stylesheet" type="text/css" href="css/component.css" />
<link rel="stylesheet" type="text/css" href="css/switch.css" />
<link rel="stylesheet" type="text/css" href="css/styles-modal.css" />
<script src="js/modernizr.custom.js"></script>
</head>
<body>
<div class="connexion-div-menu <?php echo $is_authenticated ? 'is-authenticated' : 'md-trigger'; ?>" data-login-label="Login" <?php echo $is_authenticated ? '' : 'data-modal="modal-Login"'; ?> id="accountPanel">
<span id="accountLabel"><?php echo htmlspecialchars($is_authenticated ? $session_cl_auth_user : 'Login', ENT_QUOTES, 'UTF-8'); ?></span>
<span class="connexion-actions" id="accountActions" <?php echo $is_authenticated ? '' : 'hidden'; ?>>
<a id="adminLink" href="admin.php" <?php echo $session_cl_auth_right === 'admin' ? '' : 'hidden'; ?>>Admin</a>
<a id="logoutLink" href="logout.php">Déconnexion</a>
</span>
</div>
<div class="se-switch switch-center" role="group" aria-label="Choix entre FR et EN">
<a class="se-switch__btn" href="index.php" data-site="se1">🇫🇷</a>
<a class="se-switch__btn is-active" href="index-en.php" data-site="se2" aria-current="page">🇬🇧</a>
</div>
<div class="md-modal md-effect-1" id="modal-Login">
<div class="md-content">
<h3>
<img class="float-left" src="img/icon_bops.png" width="48" height="48" alt="" />
LOGIN INTERFACE
<a class="frame-icon-close md-close float-right" href="#"><img src="img/icon_close.png" width="48" height="48" alt="" /></a>
</h3>
<div>
<p class="txt-center">Enter your credentials to access the secure area.*</p>
<form class="js-login-form" method="post" action="login.php">
<div>
<p class="txt-center"><input class="connexion-champ" id="idr" type="text" name="cl_auth_user" placeholder="RID" required /></p>
<p class="txt-center"><input class="connexion-champ" id="pwr" type="password" name="cl_auth_pass" placeholder="PASSWORD" required /></p>
</div>
<div>
<p class="txt-center"><input class="connexion-bouton" type="submit" value="Authenticate" /></p>
<p class="txt-center login-status" id="loginStatus" aria-live="polite"></p>
</div>
</form>
<p class="txt-center txt-s10">* Access under high surveillance. By confirming your credentials, you agree to the R.E.A.C.T. Initiative security protocols. Any breach of protocol is subject to sanctions.</p>
</div>
</div>
</div>
<div class="md-modal md-effect-1" id="modal-About">
<div class="md-content">
<h3>
<img class="float-left" src="img/icon_bops.png" width="48" height="48" alt="" />
MORE INFORMATIONS
<a class="frame-icon-close md-close float-right" href="#"><img src="img/icon_close.png" width="48" height="48" alt="" /></a>
</h3>
<div>
<img class="float-left" src="img/fl_security.png" width="300" height="460" alt="" />
<p>La sécurité et la protection des intérêts impériaux sont des priorités absolues pour lagence.</p>
<p>En tant que garante de lordre dans les zones sous juridiction de lUEE, lagence BOPS s'engage à prévenir, neutraliser et répondre efficacement à toutes les formes de menaces, qu'elles soient internes ou externes aux territoires de l'Empire.</p>
<p>Cette mission repose sur une approche combinant vigilance, réactivité et expertise tactique, afin dassurer la stabilité, la sécurité des citoyens et le respect des lois impériales.</p>
<p>Chaque opération est conçue pour inspirer confiance aux populations locales tout en projetant une image de force et de discipline au sein de lunivers connu.</p>
<p class="ul-style">
Surveillance active des secteurs critiques pour dissuader les menaces potentielles.</br>
Protection rapprochée de convois, personnalités importantes ou cargaisons sensibles.</br>
Neutralisation de vaisseaux hostiles ou suspects, avec récupération dactifs stratégiques.</br>
Ciblage et capture ou élimination de fugitifs dangereux, conformément aux lois impériales.</br>
Protection des civils en détresse et aide aux infrastructures endommagées.</br>
Collecte et analyse dinformations sur des activités suspectes, incidents ou organisations hostiles.
</p>
</div>
</div>
</div>
<div class="center-page-bops"></div>
<div class="center-div-menu">
<div class="padding50">
<p class="txt-center txt-bold txt-s40 small-caps padding25 txt-or">Rapid Emergency & Action Combat Team</p>
<!-- <p class="txt-center txt-bold txt-italic txt-s20 padding15 txt-or"><span class="padding5">SÉCURITÉ</span> —— <span class="padding5">INTELLIGENCE</span> —— <span class="padding5">SAUVETAGE</span></p> -->
<p class="txt-justify padding5">Founded to combat extreme criminality across the Verse. R.E.A.C.T. is an independent operational label, uniting dedicated pilots from all horizons to neutralize threats where the law is failing.</p>
<p class="txt-center txt-bold txt-s22 padding25 txt-or">—————— R.E.A.C.T. is not an organization, but a tactical standard ——————</p>
<p class="txt-justify padding5">It is a cross-org initiative designed for players who share the same vision: protecting citizens and NPCs from hostile entities. Wearing the R.E.A.C.T. badge doesnt mean leaving your organization; it means joining a rapid response network.</p>
</div>
<div class="center-div-menu-flex txt-bold">
<div class="menu-item md-trigger" data-modal="modal-About"><a href="#">> MORE INFORMATIONS <</a></div>
</div>
</div>
<div class="black-filter"></div>
<!-- Vidéo de fond -->
<div class="video-container" aria-hidden="true">
<iframe
id="react-video"
src="https://www.youtube-nocookie.com/embed/tFDqWOqm1G8?controls=0&autoplay=1&mute=1&playsinline=1&loop=1&playlist=tFDqWOqm1G8&cc_load_policy=0&iv_load_policy=3&modestbranding=1&rel=0&enablejsapi=1"
title="Présentation R.E.A.C.T — YouTube"
loading="lazy"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; fullscreen"
allowfullscreen
referrerpolicy="strict-origin-when-cross-origin">
</iframe>
</div>
<div class="md-overlay"></div><!-- the overlay element -->
<!-- classie.js by @desandro: https://github.com/desandro/classie -->
<script src="js/classie.js"></script>
<script src="js/modalEffects.js"></script>
<script src="js/auth.js"></script>
<!-- for the blur effect -->
<!-- by @derSchepp https://github.com/Schepp/CSS-Filters-Polyfill -->
<script>
// this is important for IEs
var polyfilter_scriptpath = '/js/';
</script>
<script src="js/cssParser.js"></script>
<script src="js/css-filters-polyfill.js"></script>
<script>
// On ajoute dynamiquement l'origine à l'URL de l'iframe (recommandé par l'API YouTube)
(function attachOrigin(){
const iframe = document.getElementById('react-video');
const url = new URL(iframe.src);
if (!url.searchParams.has('origin')) {
url.searchParams.set('origin', window.location.origin);
iframe.src = url.toString();
}
})();
// Utilitaires pour parler au lecteur via postMessage (API YouTube)
function ytCommand(func, args = []) {
const iframe = document.getElementById('react-video');
if (!iframe || !iframe.contentWindow) return;
iframe.contentWindow.postMessage(JSON.stringify({ event: 'command', func, args }), '*');
}
// Au premier geste utilisateur : on remet le son et on met le volume à 50 %
function enableSoundOnce() {
ytCommand('unMute');
ytCommand('setVolume', [50]);
// Masquer le bouton
const btn = document.getElementById('unmute-btn');
if (btn) btn.style.display = 'none';
// Retirer les écouteurs pour ne pas répéter laction
window.removeEventListener('pointerdown', enableSoundOnce);
window.removeEventListener('keydown', enableSoundOnce);
window.removeEventListener('touchstart', enableSoundOnce, { passive: true });
}
// Bouton dédié
// document.getElementById('unmute-btn').addEventListener('click', enableSoundOnce);
// …ou nimporte quel premier geste sur la page
//window.addEventListener('pointerdown', enableSoundOnce, { once: true });
//window.addEventListener('keydown', enableSoundOnce, { once: true });
//window.addEventListener('touchstart', enableSoundOnce, { once: true, passive: true });
</script>
<div class="footer txt-s12 txt-center">
<p>
<a href="https://robertsspaceindustries.com/en/orgs/REACT" target="_blank" title="Notre page RSI"><img src="img/icon09b.png" width="48" height="48" alt="" /></a>
<a href="https://discord.gg/dAvST8E7Mq" target="_blank" title="Notre Discord"><img src="img/icon08b.png" width="48" height="48" alt="" /></a>
<a href="https://robertsspaceindustries.com" target="_blank" title="RSI"><img src="img/icon10.png" width="48" height="48" alt="" /></a>
</p>
<p>Copyright © 2025-<?php echo date('Y'); ?> | www.react-sc.fr | All Rights Reserved.</p>
</div>
</body>
</html>

370
index.php
View File

@ -1,176 +1,194 @@
<!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">
<meta name="keywords" content="BlackOps Agency, Star Citizen, Advocacy, UEE, Sécurité, Exploration, Renseignement, Patrouilles, Chasses de primes, Investigation, Neutralisation de menaces, Assistance humanitaire, Abordages, Réseaux anti-toxicité, Formation tactique, Entraînements, Coopération, Zones UEE, Systèmes inexplorés, Lore Star Citizen, Missions multi-joueurs, Protocole Arkange, Protocole Vigilance, Respect des lois impériales, Justice interstellaire, Criminalité galactique, Piraterie, XenoThreat, Vanduul, Sécurité spatiale, Leadership BlackOps, Collaboration stratégique, Combat spatial, Star Citizen gameplay, Exploration stratégique, Missions BlackOps Agency, Réputation UEE, Gestion des conflits, BlackOps, Agency, Recrutement Star Citizen, Zones de guerre, Star Citizen immersion.">
<title>Rapid Emergency & Action Combat Team / Star Citizen</title>
<link rel="stylesheet" type="text/css" href="css/styles.css">
<link rel="stylesheet" type="text/css" href="css/default.css" />
<link rel="stylesheet" type="text/css" href="css/component.css" />
<link rel="stylesheet" type="text/css" href="css/switch.css" />
<link rel="stylesheet" type="text/css" href="css/styles-modal.css" />
<script src="js/modernizr.custom.js"></script>
</head>
<body>
<div class="assets-div-menu">
<a href="#">Contenu à venir</a>
</div>
<div class="connexion-div-menu md-trigger" data-modal="modal-Login"><a href="#">Connexion</a></div>
<div class="se-switch switch-center" role="group" aria-label="Choix entre FR et EN">
<a class="se-switch__btn is-active" href="index.php" data-site="se1" aria-current="page">🇫🇷</a>
<a class="se-switch__btn" href="index-en.php" data-site="se2">🇬🇧</a>
</div>
<div class="md-modal md-effect-1" id="modal-Login">
<div class="md-content">
<h3>
<img class="float-left" src="img/icon_bops.png" width="48" height="48" alt="" />
INTERFACE DE CONNEXION
<a class="frame-icon-close md-close float-right" href="#"><img src="img/icon_close.png" width="48" height="48" alt="" /></a>
</h3>
<div>
<p class="txt-center">Entrer vos identifiants pour acceder à l'espace sécurisé.*</p>
<form>
<div>
<p class="txt-center"><input class="connexion-champ" id="idr" type="text" name="username" placeholder="RID" /></p>
<p class="txt-center"><input class="connexion-champ" id="pwr" type="password" name="password" placeholder="MOT DE PASSE" /></p>
</div>
<div>
<p class="txt-center"><input class="connexion-bouton" type="submit" value="S'authentifier" /></p>
</div>
</form>
<p class="txt-center txt-s10">* Accès sous haute surveillance. En confirmant vos identifiants, vous acceptez les protocoles de sécurité de l'initiative R.E.A.C.T. Toute infraction aux protocoles est passible de sanctions.</p>
</div>
</div>
</div>
<div class="md-modal md-effect-1" id="modal-About">
<div class="md-content">
<h3>
<img class="float-left" src="img/icon_bops.png" width="48" height="48" alt="" />
EN SAVOIR PLUS
<a class="frame-icon-close md-close float-right" href="#"><img src="img/icon_close.png" width="48" height="48" alt="" /></a>
</h3>
<div>
<img class="float-left" src="img/fl_security.png" width="300" height="460" alt="" />
<p>La sécurité et la protection des intérêts impériaux sont des priorités absolues pour lagence.</p>
<p>En tant que garante de lordre dans les zones sous juridiction de lUEE, lagence BOPS s'engage à prévenir, neutraliser et répondre efficacement à toutes les formes de menaces, qu'elles soient internes ou externes aux territoires de l'Empire.</p>
<p>Cette mission repose sur une approche combinant vigilance, réactivité et expertise tactique, afin dassurer la stabilité, la sécurité des citoyens et le respect des lois impériales.</p>
<p>Chaque opération est conçue pour inspirer confiance aux populations locales tout en projetant une image de force et de discipline au sein de lunivers connu.</p>
<p class="ul-style">
Surveillance active des secteurs critiques pour dissuader les menaces potentielles.</br>
Protection rapprochée de convois, personnalités importantes ou cargaisons sensibles.</br>
Neutralisation de vaisseaux hostiles ou suspects, avec récupération dactifs stratégiques.</br>
Ciblage et capture ou élimination de fugitifs dangereux, conformément aux lois impériales.</br>
Protection des civils en détresse et aide aux infrastructures endommagées.</br>
Collecte et analyse dinformations sur des activités suspectes, incidents ou organisations hostiles.
</p>
</div>
</div>
</div>
<div class="center-page-bops"></div>
<div class="center-div-menu">
<div class="padding50">
<p class="txt-center txt-bold txt-s40 small-caps padding25 txt-or">Rapid Emergency & Action Combat Team</p>
<!-- <p class="txt-center txt-bold txt-italic txt-s20 padding15 txt-or"><span class="padding5">SÉCURITÉ</span> —— <span class="padding5">INTELLIGENCE</span> —— <span class="padding5">SAUVETAGE</span></p> -->
<p class="txt-justify padding5">Fondée pour combattre la criminalité extrême à travers lUnivers, la R.E.A.C.T. est un label opérationnel indépendant. Nous unissons des pilotes dévoués de tous horizons pour neutraliser les menaces la loi fait défaut.</p>
<p class="txt-center txt-bold txt-s22 padding25 txt-or">—————— La R.E.A.C.T. nest pas une organisation, mais un standard tactique ——————</p>
<p class="txt-justify padding5">Cest une initiative inter-organisations conçue pour les joueurs qui partagent la même vision : protéger les citoyens et les PNJs des entités hostiles. Porter linsigne R.E.A.C.T. ne signifie pas quitter votre organisation ; cela signifie rejoindre un réseau d'intervention rapide.</p>
</div>
<div class="center-div-menu-flex txt-bold">
<div class="menu-item md-trigger" data-modal="modal-About"><a href="#">> EN SAVOIR PLUS <</a></div>
</div>
</div>
<div class="black-filter"></div>
<!-- Vidéo de fond -->
<div class="video-container" aria-hidden="true">
<iframe
id="react-video"
src="https://www.youtube-nocookie.com/embed/tFDqWOqm1G8?controls=0&autoplay=1&mute=1&playsinline=1&loop=1&playlist=tFDqWOqm1G8&cc_load_policy=0&iv_load_policy=3&modestbranding=1&rel=0&enablejsapi=1"
title="Présentation R.E.A.C.T — YouTube"
loading="lazy"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; fullscreen"
allowfullscreen
referrerpolicy="strict-origin-when-cross-origin">
</iframe>
</div>
<div class="md-overlay"></div><!-- the overlay element -->
<!-- classie.js by @desandro: https://github.com/desandro/classie -->
<script src="js/classie.js"></script>
<script src="js/modalEffects.js"></script>
<!-- for the blur effect -->
<!-- by @derSchepp https://github.com/Schepp/CSS-Filters-Polyfill -->
<script>
// this is important for IEs
var polyfilter_scriptpath = '/js/';
</script>
<script src="js/cssParser.js"></script>
<script src="js/css-filters-polyfill.js"></script>
<script>
// On ajoute dynamiquement l'origine à l'URL de l'iframe (recommandé par l'API YouTube)
(function attachOrigin(){
const iframe = document.getElementById('react-video');
const url = new URL(iframe.src);
if (!url.searchParams.has('origin')) {
url.searchParams.set('origin', window.location.origin);
iframe.src = url.toString();
}
})();
// Utilitaires pour parler au lecteur via postMessage (API YouTube)
function ytCommand(func, args = []) {
const iframe = document.getElementById('react-video');
if (!iframe || !iframe.contentWindow) return;
iframe.contentWindow.postMessage(JSON.stringify({ event: 'command', func, args }), '*');
}
// Au premier geste utilisateur : on remet le son et on met le volume à 50 %
function enableSoundOnce() {
ytCommand('unMute');
ytCommand('setVolume', [50]);
// Masquer le bouton
const btn = document.getElementById('unmute-btn');
if (btn) btn.style.display = 'none';
// Retirer les écouteurs pour ne pas répéter laction
window.removeEventListener('pointerdown', enableSoundOnce);
window.removeEventListener('keydown', enableSoundOnce);
window.removeEventListener('touchstart', enableSoundOnce, { passive: true });
}
// Bouton dédié
// document.getElementById('unmute-btn').addEventListener('click', enableSoundOnce);
// …ou nimporte quel premier geste sur la page
//window.addEventListener('pointerdown', enableSoundOnce, { once: true });
//window.addEventListener('keydown', enableSoundOnce, { once: true });
//window.addEventListener('touchstart', enableSoundOnce, { once: true, passive: true });
</script>
<div class="footer txt-s12 txt-center">
<p>
<a href="https://robertsspaceindustries.com/en/orgs/REACT" target="_blank" title="Notre page RSI"><img src="img/icon09b.png" width="48" height="48" alt="" /></a>
<a href="https://discord.gg/dAvST8E7Mq" target="_blank" title="Notre Discord"><img src="img/icon08b.png" width="48" height="48" alt="" /></a>
<a href="https://robertsspaceindustries.com" target="_blank" title="RSI"><img src="img/icon10.png" width="48" height="48" alt="" /></a>
</p>
<p>Copyright © 2025-<?php echo date('Y'); ?> | www.react-sc.fr | Tous droits réservés.</p>
</div>
</body>
</html>
<?php
require_once __DIR__ . '/db/auth.php';
auth_start_session();
auth_bootstrap();
$session_cl_auth_user = isset($_SESSION['user']) ? (string) $_SESSION['user'] : '';
$session_cl_auth_right = isset($_SESSION['role']) ? (string) $_SESSION['role'] : '';
$is_authenticated = $session_cl_auth_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">
<meta name="keywords" content="BlackOps Agency, Star Citizen, Advocacy, UEE, Sécurité, Exploration, Renseignement, Patrouilles, Chasses de primes, Investigation, Neutralisation de menaces, Assistance humanitaire, Abordages, Réseaux anti-toxicité, Formation tactique, Entraînements, Coopération, Zones UEE, Systèmes inexplorés, Lore Star Citizen, Missions multi-joueurs, Protocole Arkange, Protocole Vigilance, Respect des lois impériales, Justice interstellaire, Criminalité galactique, Piraterie, XenoThreat, Vanduul, Sécurité spatiale, Leadership BlackOps, Collaboration stratégique, Combat spatial, Star Citizen gameplay, Exploration stratégique, Missions BlackOps Agency, Réputation UEE, Gestion des conflits, BlackOps, Agency, Recrutement Star Citizen, Zones de guerre, Star Citizen immersion.">
<title>Rapid Emergency & Action Combat Team / Star Citizen</title>
<link rel="stylesheet" type="text/css" href="css/styles.css">
<link rel="stylesheet" type="text/css" href="css/default.css" />
<link rel="stylesheet" type="text/css" href="css/component.css" />
<link rel="stylesheet" type="text/css" href="css/switch.css" />
<link rel="stylesheet" type="text/css" href="css/styles-modal.css" />
<script src="js/modernizr.custom.js"></script>
</head>
<body>
<div class="assets-div-menu">
<a href="#">Contenu à venir</a>
</div>
<div class="connexion-div-menu <?php echo $is_authenticated ? 'is-authenticated' : 'md-trigger'; ?>" data-login-label="Connexion" <?php echo $is_authenticated ? '' : 'data-modal="modal-Login"'; ?> id="accountPanel">
<span id="accountLabel"><?php echo htmlspecialchars($is_authenticated ? $session_cl_auth_user : 'Connexion', ENT_QUOTES, 'UTF-8'); ?></span>
<span class="connexion-actions" id="accountActions" <?php echo $is_authenticated ? '' : 'hidden'; ?>>
<a id="adminLink" href="admin.php" <?php echo $session_cl_auth_right === 'admin' ? '' : 'hidden'; ?>>Admin</a>
<a id="logoutLink" href="logout.php">Déconnexion</a>
</span>
</div>
<div class="se-switch switch-center" role="group" aria-label="Choix entre FR et EN">
<a class="se-switch__btn is-active" href="index.php" data-site="se1" aria-current="page">🇫🇷</a>
<a class="se-switch__btn" href="index-en.php" data-site="se2">🇬🇧</a>
</div>
<div class="md-modal md-effect-1" id="modal-Login">
<div class="md-content">
<h3>
<img class="float-left" src="img/icon_bops.png" width="48" height="48" alt="" />
INTERFACE DE CONNEXION
<a class="frame-icon-close md-close float-right" href="#"><img src="img/icon_close.png" width="48" height="48" alt="" /></a>
</h3>
<div>
<p class="txt-center">Entrer vos identifiants pour acceder à l'espace sécurisé.*</p>
<form class="js-login-form" method="post" action="login.php">
<div>
<p class="txt-center"><input class="connexion-champ" id="idr" type="text" name="cl_auth_user" placeholder="RID" required /></p>
<p class="txt-center"><input class="connexion-champ" id="pwr" type="password" name="cl_auth_pass" placeholder="MOT DE PASSE" required /></p>
</div>
<div>
<p class="txt-center"><input class="connexion-bouton" type="submit" value="S'authentifier" /></p>
<p class="txt-center login-status" id="loginStatus" aria-live="polite"></p>
</div>
</form>
<p class="txt-center txt-s10">* Accès sous haute surveillance. En confirmant vos identifiants, vous acceptez les protocoles de sécurité de l'initiative R.E.A.C.T. Toute infraction aux protocoles est passible de sanctions.</p>
</div>
</div>
</div>
<div class="md-modal md-effect-1" id="modal-About">
<div class="md-content">
<h3>
<img class="float-left" src="img/icon_bops.png" width="48" height="48" alt="" />
EN SAVOIR PLUS
<a class="frame-icon-close md-close float-right" href="#"><img src="img/icon_close.png" width="48" height="48" alt="" /></a>
</h3>
<div>
<img class="float-left" src="img/fl_security.png" width="300" height="460" alt="" />
<p>La sécurité et la protection des intérêts impériaux sont des priorités absolues pour lagence.</p>
<p>En tant que garante de lordre dans les zones sous juridiction de lUEE, lagence BOPS s'engage à prévenir, neutraliser et répondre efficacement à toutes les formes de menaces, qu'elles soient internes ou externes aux territoires de l'Empire.</p>
<p>Cette mission repose sur une approche combinant vigilance, réactivité et expertise tactique, afin dassurer la stabilité, la sécurité des citoyens et le respect des lois impériales.</p>
<p>Chaque opération est conçue pour inspirer confiance aux populations locales tout en projetant une image de force et de discipline au sein de lunivers connu.</p>
<p class="ul-style">
Surveillance active des secteurs critiques pour dissuader les menaces potentielles.</br>
Protection rapprochée de convois, personnalités importantes ou cargaisons sensibles.</br>
Neutralisation de vaisseaux hostiles ou suspects, avec récupération dactifs stratégiques.</br>
Ciblage et capture ou élimination de fugitifs dangereux, conformément aux lois impériales.</br>
Protection des civils en détresse et aide aux infrastructures endommagées.</br>
Collecte et analyse dinformations sur des activités suspectes, incidents ou organisations hostiles.
</p>
</div>
</div>
</div>
<div class="center-page-bops"></div>
<div class="center-div-menu">
<div class="padding50">
<p class="txt-center txt-bold txt-s40 small-caps padding25 txt-or">Rapid Emergency & Action Combat Team</p>
<!-- <p class="txt-center txt-bold txt-italic txt-s20 padding15 txt-or"><span class="padding5">SÉCURITÉ</span> —— <span class="padding5">INTELLIGENCE</span> —— <span class="padding5">SAUVETAGE</span></p> -->
<p class="txt-justify padding5">Fondée pour combattre la criminalité extrême à travers lUnivers, la R.E.A.C.T. est un label opérationnel indépendant. Nous unissons des pilotes dévoués de tous horizons pour neutraliser les menaces la loi fait défaut.</p>
<p class="txt-center txt-bold txt-s22 padding25 txt-or">—————— La R.E.A.C.T. nest pas une organisation, mais un standard tactique ——————</p>
<p class="txt-justify padding5">Cest une initiative inter-organisations conçue pour les joueurs qui partagent la même vision : protéger les citoyens et les PNJs des entités hostiles. Porter linsigne R.E.A.C.T. ne signifie pas quitter votre organisation ; cela signifie rejoindre un réseau d'intervention rapide.</p>
</div>
<div class="center-div-menu-flex txt-bold">
<div class="menu-item md-trigger" data-modal="modal-About"><a href="#">> EN SAVOIR PLUS <</a></div>
</div>
</div>
<div class="black-filter"></div>
<!-- Vidéo de fond -->
<div class="video-container" aria-hidden="true">
<iframe
id="react-video"
src="https://www.youtube-nocookie.com/embed/tFDqWOqm1G8?controls=0&autoplay=1&mute=1&playsinline=1&loop=1&playlist=tFDqWOqm1G8&cc_load_policy=0&iv_load_policy=3&modestbranding=1&rel=0&enablejsapi=1"
title="Présentation R.E.A.C.T — YouTube"
loading="lazy"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; fullscreen"
allowfullscreen
referrerpolicy="strict-origin-when-cross-origin">
</iframe>
</div>
<div class="md-overlay"></div><!-- the overlay element -->
<!-- classie.js by @desandro: https://github.com/desandro/classie -->
<script src="js/classie.js"></script>
<script src="js/modalEffects.js"></script>
<script src="js/auth.js"></script>
<!-- for the blur effect -->
<!-- by @derSchepp https://github.com/Schepp/CSS-Filters-Polyfill -->
<script>
// this is important for IEs
var polyfilter_scriptpath = '/js/';
</script>
<script src="js/cssParser.js"></script>
<script src="js/css-filters-polyfill.js"></script>
<script>
// On ajoute dynamiquement l'origine à l'URL de l'iframe (recommandé par l'API YouTube)
(function attachOrigin(){
const iframe = document.getElementById('react-video');
const url = new URL(iframe.src);
if (!url.searchParams.has('origin')) {
url.searchParams.set('origin', window.location.origin);
iframe.src = url.toString();
}
})();
// Utilitaires pour parler au lecteur via postMessage (API YouTube)
function ytCommand(func, args = []) {
const iframe = document.getElementById('react-video');
if (!iframe || !iframe.contentWindow) return;
iframe.contentWindow.postMessage(JSON.stringify({ event: 'command', func, args }), '*');
}
// Au premier geste utilisateur : on remet le son et on met le volume à 50 %
function enableSoundOnce() {
ytCommand('unMute');
ytCommand('setVolume', [50]);
// Masquer le bouton
const btn = document.getElementById('unmute-btn');
if (btn) btn.style.display = 'none';
// Retirer les écouteurs pour ne pas répéter laction
window.removeEventListener('pointerdown', enableSoundOnce);
window.removeEventListener('keydown', enableSoundOnce);
window.removeEventListener('touchstart', enableSoundOnce, { passive: true });
}
// Bouton dédié
// document.getElementById('unmute-btn').addEventListener('click', enableSoundOnce);
// …ou nimporte quel premier geste sur la page
//window.addEventListener('pointerdown', enableSoundOnce, { once: true });
//window.addEventListener('keydown', enableSoundOnce, { once: true });
//window.addEventListener('touchstart', enableSoundOnce, { once: true, passive: true });
</script>
<div class="footer txt-s12 txt-center">
<p>
<a href="https://robertsspaceindustries.com/en/orgs/REACT" target="_blank" title="Notre page RSI"><img src="img/icon09b.png" width="48" height="48" alt="" /></a>
<a href="https://discord.gg/dAvST8E7Mq" target="_blank" title="Notre Discord"><img src="img/icon08b.png" width="48" height="48" alt="" /></a>
<a href="https://robertsspaceindustries.com" target="_blank" title="RSI"><img src="img/icon10.png" width="48" height="48" alt="" /></a>
</p>
<p>Copyright © 2025-<?php echo date('Y'); ?> | www.react-sc.fr | Tous droits réservés.</p>
</div>
</body>
</html>

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";
}
?>

122
js/auth.js Normal file
View File

@ -0,0 +1,122 @@
(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 = 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;
}
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(closeModal, 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);
})
.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;

503
scitems.php Normal file
View File

@ -0,0 +1,503 @@
<?php
require_once __DIR__ . '/db/auth.php';
auth_start_session();
auth_bootstrap();
if (!auth_is_admin()) {
header('Location: index.php');
exit;
}
$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) VALUES (:name, :type, :subtype, :uuid, '', :about)");
$stmt_update = $db->prepare("UPDATE tbl_scobjs SET cl_scobjs_name = :name, cl_scobjs_type = :type, cl_scobjs_subtype = :subtype, cl_scobjs_about = :about WHERE cl_scobjs_uuid = :uuid");
foreach ($items as $item) {
$uuid = $item['reference'] ?? ($item['stdItem']['UUID'] ?? '');
if (!$uuid) continue;
$manufacturer = $item['manufacturer'] ?? ($item['stdItem']['Manufacturer']['Code'] ?? '');
$raw_name = $item['stdItem']['Name'] ?? '';
$name = ($manufacturer && strpos($raw_name, $manufacturer) === false) ? "[$manufacturer] $raw_name" : $raw_name;
$type = $item['type'] ?? '';
$subtype = $item['subType'] ?? '';
$about = $item['stdItem']['Description'] ?? '';
$stmt_check->execute(['uuid' => $uuid]);
if ($stmt_check->fetch()) {
$stmt_update->execute([
'name' => $name,
'type' => $type,
'subtype' => $subtype,
'about' => $about,
'uuid' => $uuid
]);
$count_updated++;
} else {
$stmt_insert->execute([
'name' => $name,
'type' => $type,
'subtype' => $subtype,
'uuid' => $uuid,
'about' => $about
]);
$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.');
header('Location: scitems.php?page=' . $page . '&search=' . urlencode($search));
exit;
}
}
// Fetch data
$stmt_count = $db->prepare("SELECT COUNT(*) FROM tbl_scobjs WHERE $where");
$stmt_count->execute($params);
$total_items = $stmt_count->fetchColumn();
$total_pages = ceil($total_items / $limit);
$stmt_list = $db->prepare("SELECT * FROM tbl_scobjs WHERE $where ORDER BY cl_scobjs_id DESC LIMIT $limit OFFSET $offset");
$stmt_list->execute($params);
$items_list = $stmt_list->fetchAll();
$edit_item = null;
if (isset($_GET['edit'])) {
$stmt_edit = $db->prepare("SELECT * FROM tbl_scobjs WHERE cl_scobjs_id = :id");
$stmt_edit->execute(['id' => (int)$_GET['edit']]);
$edit_item = $stmt_edit->fetch();
}
$current_session_user = $_SESSION['user_name'] ?? 'Admin';
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Admin - Base d'Objets</title>
<link href="https://fonts.googleapis.com/css2?family=Electrolize&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/styles.css">
<style>
:root {
--primary: #a29b78;
--primary-glow: rgba(162, 155, 120, 0.3);
--bg-dark: #0a0b0d;
--card-bg: rgba(20, 22, 26, 0.8);
--border-glow: rgba(162, 155, 120, 0.2);
--rarity-L: #ff8000;
--rarity-E: #a335ee;
--rarity-R: #0070dd;
--rarity-U: #1eff00;
--rarity-C: #ffffff;
--danger: #ff4d4d;
--success: #00ff88;
}
body {
background: var(--bg-dark);
color: #e0e0e0;
font-family: 'Electrolize', sans-serif;
margin: 0;
line-height: 1.6;
background-image:
radial-gradient(circle at 50% 50%, rgba(162, 155, 120, 0.05) 0%, transparent 50%),
linear-gradient(rgba(10, 11, 13, 0.9), rgba(10, 11, 13, 0.9));
}
.admin-layout { max-width: 1400px; margin: 0 auto; padding: 2rem; }
.admin-topbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border-glow);
}
.topbar-info h1 { margin: 0; font-size: 1.5rem; color: var(--primary); text-transform: uppercase; letter-spacing: 2px; }
.topbar-info p { margin: 5px 0 0; font-size: 0.8rem; opacity: 0.7; }
.btn-modern {
background: rgba(162, 155, 120, 0.1);
border: 1px solid var(--primary);
color: var(--primary);
padding: 0.6rem 1.2rem;
border-radius: 4px;
cursor: pointer;
text-transform: uppercase;
font-size: 0.8rem;
letter-spacing: 1px;
transition: all 0.3s ease;
text-decoration: none;
display: inline-block;
}
.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; }
.glass-card {
background: var(--card-bg);
border: 1px solid var(--border-glow);
border-radius: 12px;
padding: 1.5rem;
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
}
.admin-grid { display: grid; grid-template-columns: 350px 1fr; gap: 2rem; }
.form-group { margin-bottom: 1.2rem; }
.form-group label { display: block; margin-bottom: 0.5rem; font-size: 0.8rem; color: var(--primary); text-transform: uppercase; }
.form-control {
width: 100%;
background: rgba(0,0,0,0.3);
border: 1px solid var(--border-glow);
border-radius: 4px;
padding: 0.8rem;
color: #fff;
font-family: inherit;
box-sizing: border-box;
}
.modern-table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
.modern-table th { text-align: left; padding: 1rem; border-bottom: 2px solid var(--border-glow); color: var(--primary); font-size: 0.8rem; text-transform: uppercase; }
.modern-table td { padding: 1rem; border-bottom: 1px solid rgba(162, 155, 120, 0.1); vertical-align: top; }
.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; white-space: pre-line; }
/* 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>
<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> | Session : <strong><?php echo htmlspecialchars($current_session_user); ?></strong></p>
</div>
<div class="topbar-actions">
<a href="index.php" class="btn-modern">Site</a>
<a href="logout.php" class="btn-modern danger">Exit</a>
</div>
</header>
<nav class="nav-tabs">
<a href="admin.php">Utilisateurs</a>
<a href="scitems.php" class="active">Base d'Objets</a>
<a href="scmining.php">Scanner Minage</a>
<a href="scpreset.php">Presets Vaisseau</a>
</nav>
<?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): ?>
<tr>
<td>
<div class="preview-container">
<img src="https://sc-item-icons.s3.amazonaws.com/<?php echo $item['cl_scobjs_uuid']; ?>.png"
class="item-preview"
onerror="this.src='img/icon10.png';"
alt="">
<div class="preview-floating">
<img src="https://sc-item-icons.s3.amazonaws.com/<?php echo $item['cl_scobjs_uuid']; ?>.png"
onerror="this.src='img/icon10.png';"
alt="">
</div>
</div>
</td>
<td>
<span class="item-name <?php echo $item['cl_scobjs_rarity'] ? 'rarity-'.$item['cl_scobjs_rarity'] : ''; ?>">
<?php echo htmlspecialchars($item['cl_scobjs_name']); ?>
</span>
<span class="item-uuid"><?php echo htmlspecialchars($item['cl_scobjs_uuid']); ?></span>
<span class="item-meta">
<span class="badge"><?php echo htmlspecialchars($item['cl_scobjs_type']); ?></span>
<?php if ($item['cl_scobjs_subtype']): ?>
<span class="badge" style="background: rgba(162,155,120,0.1);"><?php echo htmlspecialchars($item['cl_scobjs_subtype']); ?></span>
<?php endif; ?>
</span>
</td>
<td class="item-about-cell">
<?php echo 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" style="padding: 0.4rem 0.8rem;">Edit</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<?php if ($total_pages > 1): ?>
<div class="pagination">
<?php if ($page > 1): ?>
<a href="scitems.php?page=<?php echo $page - 1; ?>&search=<?php echo urlencode($search); ?>" class="page-link">&laquo;</a>
<?php endif; ?>
<?php
$start_page = max(1, $page - 2);
$end_page = min($total_pages, $page + 2);
for ($i = $start_page; $i <= $end_page; $i++):
?>
<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 endfor; ?>
<?php if ($page < $total_pages): ?>
<a href="scitems.php?page=<?php echo $page + 1; ?>&search=<?php echo urlencode($search); ?>" class="page-link">&raquo;</a>
<?php endif; ?>
</div>
<?php endif; ?>
</section>
</main>
</div>
</div>
</body>
</html>

453
scmining.php Normal file
View File

@ -0,0 +1,453 @@
<?php
require_once __DIR__ . '/db/auth.php';
auth_start_session();
auth_bootstrap();
if (!auth_is_admin()) {
header('Location: index.php');
exit;
}
$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'];
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.php');
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
$search = isset($_GET['search']) ? trim($_GET['search']) : '';
$search_results = [];
if ($search !== '') {
$stmt_search = $db->prepare("SELECT * FROM tbl_scobjs WHERE cl_scobjs_name LIKE :search AND cl_scobjs_id NOT IN (SELECT cl_scmining_obj_id FROM tbl_scmining) ORDER BY cl_scobjs_name ASC LIMIT 10");
$stmt_search->execute(['search' => "%$search%"]);
$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
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;
}
@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;
}
.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; }
.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-meta { display: block; font-size: 0.75rem; color: #888; }
.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>
<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> | Session : <strong><?php echo htmlspecialchars($current_session_user); ?></strong></p>
</div>
<div class="topbar-actions">
<a href="index.php" class="btn-modern">Site</a>
<a href="logout.php" class="btn-modern danger">Exit</a>
</div>
</header>
<nav class="nav-tabs">
<a href="admin.php">Utilisateurs</a>
<a href="scitems.php">Base d'Objets</a>
<a href="scmining.php" class="active">Scanner Minage</a>
<a href="scpreset.php">Presets Vaisseau</a>
</nav>
<?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($search_results)): ?>
<p style="text-align: center; color: #666;">Aucun objet non listé trouvé.</p>
<?php else: ?>
<?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'])) {
?>
<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($res['cl_scobjs_name']); ?></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']; ?>">
<button type="submit" class="btn-modern btn-mini">+</button>
</form>
</div>
<?php } endforeach; ?>
<?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'])) {
?>
<tr>
<td>
<img src="https://cstone.space/uifimages/<?php echo $item['cl_scobjs_uuid']; ?>.png" class="item-preview" alt="">
</td>
<td>
<strong style="color: var(--primary);"><?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>

468
scpreset.php Normal file
View File

@ -0,0 +1,468 @@
<?php
require_once __DIR__ . '/db/auth.php';
auth_start_session();
auth_bootstrap();
if (!auth_is_logged_in()) {
header('Location: index.php');
exit;
}
$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';
// 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') {
$name = trim($_POST['name'] ?? '');
$manufacturer = trim($_POST['manufacturer'] ?? '');
$description = trim($_POST['description'] ?? '');
$link = trim($_POST['link'] ?? '');
$creator = $current_session_user ?: 'Inconnu';
if ($name !== '' && $manufacturer !== '' && $link !== '') {
try {
$stmt = $db->prepare("INSERT INTO tbl_scpreset (cl_scpreset_name, cl_scpreset_manufacturer, cl_scpreset_description, cl_scpreset_link, cl_scpreset_creator) VALUES (:name, :manufacturer, :description, :link, :creator)");
$stmt->execute([
'name' => $name,
'manufacturer' => $manufacturer,
'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 remplir les champs obligatoires (Nom, Manufacture, Lien).');
}
header('Location: scpreset.php');
exit;
}
// Update preset
if ($action === 'update_preset') {
$preset_id = (int)($_POST['preset_id'] ?? 0);
$name = trim($_POST['name'] ?? '');
$manufacturer = trim($_POST['manufacturer'] ?? '');
$description = trim($_POST['description'] ?? '');
$link = trim($_POST['link'] ?? '');
if ($preset_id > 0 && $name !== '' && $manufacturer !== '' && $link !== '') {
try {
$stmt = $db->prepare("UPDATE tbl_scpreset SET cl_scpreset_name = :name, cl_scpreset_manufacturer = :manufacturer, cl_scpreset_description = :description, cl_scpreset_link = :link WHERE cl_scpreset_id = :id");
$stmt->execute([
'name' => $name,
'manufacturer' => $manufacturer,
'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.');
}
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;
}
}
// Fetch all presets
$stmt_list = $db->query("SELECT * FROM tbl_scpreset ORDER BY cl_scpreset_manufacturer ASC, cl_scpreset_name ASC");
$presets = $stmt_list->fetchAll();
?>
<!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;
}
.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; }
.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>
<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> | Session : <strong><?php echo htmlspecialchars($current_session_user); ?></strong></p>
</div>
<div class="topbar-actions">
<a href="index.php" class="btn-modern">Site</a>
<a href="logout.php" class="btn-modern danger">Exit</a>
</div>
</header>
<nav class="nav-tabs">
<?php if (auth_is_admin()): ?>
<a href="admin.php">Utilisateurs</a>
<a href="scitems.php">Base d\'Objets</a>
<a href="scmining.php">Scanner Minage</a>
<?php endif; ?>
<a href="scpreset.php" class="active">Presets Vaisseau</a>
</nav>
<?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>
<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>Nom du Vaisseau</label>
<input type="text" name="name" id="presetName" class="form-control" required placeholder="ex: Prospector">
</div>
<div class="form-group">
<label>Manufacture</label>
<input type="text" name="manufacturer" id="presetManufacturer" class="form-control" required placeholder="ex: MISC">
</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>
</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['cl_scpreset_name']); ?></strong><br>
<span class="manufacturer-text"><?php echo htmlspecialchars($p['cl_scpreset_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"],
"name" => $p["cl_scpreset_name"],
"manufacturer" => $p["cl_scpreset_manufacturer"],
"description" => $p["cl_scpreset_description"],
"link" => $p["cl_scpreset_link"]
]); ?>)'>
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>
function editPreset(data) {
document.getElementById('formAction').value = 'update_preset';
document.getElementById('presetId').value = data.id;
document.getElementById('presetName').value = data.name;
document.getElementById('presetManufacturer').value = data.manufacturer;
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 = 'Modifier le Preset';
// Scroll to form
document.getElementById('presetForm').scrollIntoView({ behavior: 'smooth' });
}
function resetForm() {
document.getElementById('formAction').value = 'add_preset';
document.getElementById('presetId').value = '';
document.getElementById('presetForm').reset();
document.getElementById('submitBtn').innerText = 'Ajouter';
document.getElementById('cancelBtn').style.display = 'none';
document.getElementById('formTitle').innerText = 'Nouveau Preset';
}
</script>
</body>
</html>