Compare commits
3 Commits
eb080f3ce5
...
6bf89f26bb
Author | SHA1 | Date | |
---|---|---|---|
6bf89f26bb | |||
771d9f3310 | |||
dcf1bc7a1d |
59
admin/api/story-access.php
Normal file
59
admin/api/story-access.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
require_once '../../includes/config.php';
|
||||
require_once '../../includes/auth.php';
|
||||
require_once '../../includes/stories.php';
|
||||
|
||||
// Vérification de l'authentification
|
||||
if (!Auth::check()) {
|
||||
http_response_code(401);
|
||||
exit(json_encode(['success' => false, 'error' => 'Non autorisé']));
|
||||
}
|
||||
|
||||
// Seul un administrateur peut modifier les autorisations d'accès
|
||||
if (!Auth::isAdmin() && !Auth::hasAdminRole()) {
|
||||
http_response_code(403);
|
||||
exit(json_encode(['success' => false, 'error' => 'Accès refusé']));
|
||||
}
|
||||
|
||||
// Vérification de la méthode HTTP
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
exit(json_encode(['success' => false, 'error' => 'Méthode non autorisée']));
|
||||
}
|
||||
|
||||
// Récupération et validation des données
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$storyId = $input['storyId'] ?? null;
|
||||
$access = $input['access'] ?? [];
|
||||
|
||||
if (!$storyId) {
|
||||
http_response_code(400);
|
||||
exit(json_encode(['success' => false, 'error' => 'ID du roman manquant']));
|
||||
}
|
||||
|
||||
try {
|
||||
// Récupération du roman
|
||||
$story = Stories::get($storyId);
|
||||
if (!$story) {
|
||||
throw new Exception('Roman non trouvé');
|
||||
}
|
||||
|
||||
// Mise à jour de la liste d'accès
|
||||
$story['access'] = $access;
|
||||
|
||||
// Sauvegarde des modifications
|
||||
Stories::save($story);
|
||||
|
||||
// Réponse de succès
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Autorisations d\'accès mises à jour avec succès'
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => 'Erreur : ' . $e->getMessage()
|
||||
]);
|
||||
}
|
@ -10,6 +10,16 @@ if (!Auth::check()) {
|
||||
}
|
||||
|
||||
$stories = Stories::getAll();
|
||||
|
||||
// Filtrer les histoires auxquelles l'utilisateur a accès
|
||||
if (!Auth::isAdmin()) {
|
||||
$stories = array_filter($stories, function($story) {
|
||||
return Auth::canAccessStory($story['id']);
|
||||
});
|
||||
}
|
||||
|
||||
// Obtenir la liste des utilisateurs pour la modale d'accès
|
||||
$users = Auth::getAllUsers(false);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
@ -48,9 +58,13 @@ $stories = Stories::getAll();
|
||||
<?php endif; ?>
|
||||
<span>Administration</span>
|
||||
</div>
|
||||
<!-- Le bouton hamburger sera inséré par JS -->
|
||||
<div class="nav-menu">
|
||||
<a href="../index.php" target="_blank" class="button">Visiter le site</a>
|
||||
<a href="profile.php" class="button">Profil</a>
|
||||
<?php if (Auth::isAdmin() || Auth::hasAdminRole()): ?>
|
||||
<a href="users.php" class="button">Utilisateurs</a>
|
||||
<?php endif; ?>
|
||||
<a href="story-edit.php" class="button">Nouveau roman</a>
|
||||
<a href="options.php" class="button">Options</a>
|
||||
<a href="export-import.php" class="button">Import/Export</a>
|
||||
@ -66,7 +80,10 @@ $stories = Stories::getAll();
|
||||
<div class="stories-list">
|
||||
<?php foreach ($stories as $story): ?>
|
||||
<div class="story-item">
|
||||
<img src="<?= htmlspecialchars('../' . $story['cover']) ?>" alt="" class="story-cover">
|
||||
<img src="<?= htmlspecialchars('../' . $story['cover']) ?>"
|
||||
alt="Couverture de <?= htmlspecialchars($story['title']) ?>"
|
||||
class="story-cover"
|
||||
loading="lazy">
|
||||
<div class="story-info">
|
||||
<h2><?= htmlspecialchars($story['title']) ?></h2>
|
||||
<p>
|
||||
@ -76,6 +93,9 @@ $stories = Stories::getAll();
|
||||
</div>
|
||||
<div class="story-actions">
|
||||
<a href="story-edit.php?id=<?= htmlspecialchars($story['id']) ?>" class="button">Modifier</a>
|
||||
<?php if (Auth::isAdmin() || Auth::hasAdminRole()): ?>
|
||||
<button type="button" class="button manage-access" data-id="<?= htmlspecialchars($story['id']) ?>">Accès</button>
|
||||
<?php endif; ?>
|
||||
<button type="button" class="button delete-story" data-id="<?= htmlspecialchars($story['id']) ?>">Supprimer</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -83,6 +103,41 @@ $stories = Stories::getAll();
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Modale de gestion des accès -->
|
||||
<?php if (Auth::isAdmin() || Auth::hasAdminRole()): ?>
|
||||
<div id="accessModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>Gérer les accès</h2>
|
||||
<p id="modalStoryTitle"></p>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Sélectionnez les utilisateurs qui auront accès à ce roman :</p>
|
||||
<div class="users-access-list">
|
||||
<?php foreach ($users as $user): ?>
|
||||
<div class="user-access-item">
|
||||
<label>
|
||||
<input type="checkbox" name="user_access[]" value="<?= htmlspecialchars($user['id']) ?>"
|
||||
<?= $user['isAdmin'] ? 'checked disabled' : '' ?>>
|
||||
<?= htmlspecialchars($user['id']) ?>
|
||||
<?php if ($user['role'] === 'admin'): ?>
|
||||
<span class="user-role-badge admin">Admin</span>
|
||||
<?php else: ?>
|
||||
<span class="user-role-badge editor">Éditeur</span>
|
||||
<?php endif; ?>
|
||||
</label>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="button dark" id="cancelAccess">Annuler</button>
|
||||
<button type="button" class="button" id="saveAccess">Enregistrer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<script src="../assets/js/admin.js"></script>
|
||||
<link rel="stylesheet" href="../assets/css/dialog.css">
|
||||
<script src="../assets/js/dialog.js"></script>
|
||||
|
@ -20,6 +20,12 @@ if (isset($_GET['id'])) {
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification des permissions d'accès
|
||||
if (!Auth::isAdmin() && !Auth::canAccessStory($_GET['id'])) {
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Traitement de la sauvegarde
|
||||
@ -35,6 +41,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
'chapters' => $story['chapters'] ?? []
|
||||
];
|
||||
|
||||
// Gestion des permissions d'accès
|
||||
if ((Auth::isAdmin() || Auth::hasAdminRole()) && isset($_POST['story_access'])) {
|
||||
try {
|
||||
$storyData['access'] = json_decode($_POST['story_access'], true) ?? [];
|
||||
} catch (Exception $e) {
|
||||
// En cas d'erreur de parsing, on utilise un tableau vide
|
||||
$storyData['access'] = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Gestion de l'upload de couverture
|
||||
if (isset($_FILES['cover']) && $_FILES['cover']['error'] !== UPLOAD_ERR_NO_FILE) {
|
||||
$uploadHandler = new CoverUploadHandler();
|
||||
@ -67,6 +83,12 @@ function generateSlug($title) {
|
||||
<?php endif; ?>
|
||||
<link rel="stylesheet" href="../assets/css/main.css">
|
||||
<link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">
|
||||
<?php if (Auth::isAdmin() || Auth::hasAdminRole()): ?>
|
||||
<script>
|
||||
// Cette variable sera utilisée par la modale de gestion des accès
|
||||
const storyAccess = <?= json_encode($story['access'] ?? []) ?>;
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="admin-nav">
|
||||
@ -108,7 +130,7 @@ function generateSlug($title) {
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">Description</label>
|
||||
<input type="hidden" id="description" name="description" required>
|
||||
<input type="hidden" id="description" name="description">
|
||||
<div id="descriptionEditor"></div>
|
||||
</div>
|
||||
|
||||
@ -287,9 +309,30 @@ function generateSlug($title) {
|
||||
const description = document.querySelector('#description');
|
||||
description.value = descriptionEditor.root.innerHTML;
|
||||
});
|
||||
|
||||
// Fonction de notification pour l'éditeur
|
||||
function showNotification(message, type = 'success') {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification ${type}`;
|
||||
notification.textContent = message;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.style.opacity = '1';
|
||||
notification.style.transform = 'translateY(0)';
|
||||
}, 10);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.style.opacity = '0';
|
||||
notification.style.transform = 'translateY(-100%)';
|
||||
setTimeout(() => notification.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<link rel="stylesheet" href="../assets/css/dialog.css">
|
||||
<script src="../assets/js/dialog.js"></script>
|
||||
</script>
|
||||
|
||||
<link rel="stylesheet" href="../assets/css/dialog.css">
|
||||
<script src="../assets/js/dialog.js"></script>
|
||||
</body>
|
||||
</html>
|
510
admin/users.php
Normal file
510
admin/users.php
Normal file
@ -0,0 +1,510 @@
|
||||
<?php
|
||||
require_once '../includes/config.php';
|
||||
require_once '../includes/auth.php';
|
||||
|
||||
// Vérification de l'authentification
|
||||
if (!Auth::check()) {
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification des privilèges administrateur (premier utilisateur uniquement)
|
||||
if (!Auth::isAdmin()) {
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$success = '';
|
||||
$error = '';
|
||||
|
||||
// Traitement de l'ajout ou modification d'un utilisateur
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||
try {
|
||||
$config = Config::load();
|
||||
|
||||
// Traitement selon l'action
|
||||
switch ($_POST['action']) {
|
||||
case 'add':
|
||||
// Validation
|
||||
$userId = trim($_POST['user_id'] ?? '');
|
||||
$password = $_POST['password'] ?? '';
|
||||
$confirmPassword = $_POST['confirm_password'] ?? '';
|
||||
$comment = trim($_POST['comment'] ?? '');
|
||||
$role = $_POST['role'] ?? 'editor';
|
||||
|
||||
if (strlen($userId) < 3) {
|
||||
throw new Exception('L\'identifiant doit faire au moins 3 caractères');
|
||||
}
|
||||
|
||||
if (strlen($password) < 8) {
|
||||
throw new Exception('Le mot de passe doit faire au moins 8 caractères');
|
||||
}
|
||||
|
||||
if ($password !== $confirmPassword) {
|
||||
throw new Exception('Les mots de passe ne correspondent pas');
|
||||
}
|
||||
|
||||
// Vérification de l'unicité de l'identifiant
|
||||
foreach ($config['users'] as $user) {
|
||||
if ($user['id'] === $userId) {
|
||||
throw new Exception('Cet identifiant est déjà utilisé');
|
||||
}
|
||||
}
|
||||
|
||||
// Ajout de l'utilisateur
|
||||
$config['users'][] = [
|
||||
'id' => $userId,
|
||||
'password' => password_hash($password, PASSWORD_DEFAULT),
|
||||
'role' => $role,
|
||||
'comment' => $comment
|
||||
];
|
||||
|
||||
$success = 'Utilisateur ajouté avec succès';
|
||||
break;
|
||||
|
||||
case 'edit':
|
||||
// Validation
|
||||
$userId = trim($_POST['user_id'] ?? '');
|
||||
$newUserId = trim($_POST['new_user_id'] ?? '');
|
||||
$password = $_POST['password'] ?? '';
|
||||
$comment = trim($_POST['comment'] ?? '');
|
||||
$role = $_POST['role'] ?? 'editor';
|
||||
|
||||
if ($newUserId && strlen($newUserId) < 3) {
|
||||
throw new Exception('Le nouvel identifiant doit faire au moins 3 caractères');
|
||||
}
|
||||
|
||||
if ($password && strlen($password) < 8) {
|
||||
throw new Exception('Le mot de passe doit faire au moins 8 caractères');
|
||||
}
|
||||
|
||||
// Recherche de l'utilisateur à modifier
|
||||
$userFound = false;
|
||||
foreach ($config['users'] as &$user) {
|
||||
if ($user['id'] === $userId) {
|
||||
// Vérification que le premier compte ne perde pas ses privilèges admin
|
||||
if ($user === reset($config['users']) && $role !== 'admin') {
|
||||
throw new Exception('Le premier compte doit conserver le rôle d\'administrateur');
|
||||
}
|
||||
|
||||
// Mise à jour des informations
|
||||
if ($newUserId && $newUserId !== $userId) {
|
||||
// Vérification de l'unicité du nouvel identifiant
|
||||
foreach ($config['users'] as $existingUser) {
|
||||
if ($existingUser['id'] === $newUserId) {
|
||||
throw new Exception('Ce nouvel identifiant est déjà utilisé');
|
||||
}
|
||||
}
|
||||
$user['id'] = $newUserId;
|
||||
}
|
||||
|
||||
if ($password) {
|
||||
$user['password'] = password_hash($password, PASSWORD_DEFAULT);
|
||||
}
|
||||
|
||||
$user['comment'] = $comment;
|
||||
$user['role'] = $role;
|
||||
$userFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$userFound) {
|
||||
throw new Exception('Utilisateur non trouvé');
|
||||
}
|
||||
|
||||
$success = 'Utilisateur modifié avec succès';
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
// Validation
|
||||
$userId = trim($_POST['user_id'] ?? '');
|
||||
|
||||
// Empêcher la suppression du premier utilisateur
|
||||
if ($userId === reset($config['users'])['id']) {
|
||||
throw new Exception('Le compte administrateur principal ne peut pas être supprimé');
|
||||
}
|
||||
|
||||
// Recherche et suppression de l'utilisateur
|
||||
$userIndex = -1;
|
||||
foreach ($config['users'] as $index => $user) {
|
||||
if ($user['id'] === $userId) {
|
||||
$userIndex = $index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($userIndex === -1) {
|
||||
throw new Exception('Utilisateur non trouvé');
|
||||
}
|
||||
|
||||
array_splice($config['users'], $userIndex, 1);
|
||||
$success = 'Utilisateur supprimé avec succès';
|
||||
break;
|
||||
}
|
||||
|
||||
// Sauvegarde de la configuration
|
||||
Config::save($config);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$error = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// Chargement de la configuration
|
||||
$config = Config::load();
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Gestion des utilisateurs - Administration</title>
|
||||
<?php if (file_exists(__DIR__ . '/../assets/images/site/favicon.png')): ?>
|
||||
<link rel="icon" type="image/png" href="../assets/images/site/favicon.png">
|
||||
<?php endif; ?>
|
||||
<link rel="stylesheet" href="../assets/css/main.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="admin-nav">
|
||||
<div class="nav-brand">
|
||||
<?php if (!empty($config['site']['logo'])): ?>
|
||||
<img src="<?= htmlspecialchars('../' . $config['site']['logo']) ?>"
|
||||
alt="<?= htmlspecialchars($config['site']['name']) ?>">
|
||||
<?php endif; ?>
|
||||
<span>Administration</span>
|
||||
</div>
|
||||
<div class="nav-menu">
|
||||
<a href="index.php" class="button">Retour</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="admin-main">
|
||||
<h1>Gestion des utilisateurs</h1>
|
||||
|
||||
<?php if ($success): ?>
|
||||
<div class="success-message"><?= htmlspecialchars($success) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="error-message"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Liste des utilisateurs existants -->
|
||||
<section class="users-section">
|
||||
<h2>Utilisateurs existants</h2>
|
||||
<div class="users-list">
|
||||
<?php foreach ($config['users'] as $index => $user): ?>
|
||||
<div class="user-item <?= $index === 0 ? 'admin-user' : '' ?>">
|
||||
<div class="user-info">
|
||||
<h3><?= htmlspecialchars($user['id']) ?></h3>
|
||||
<p class="user-role">
|
||||
Rôle : <span><?= htmlspecialchars($user['role'] ?? 'editor') ?></span>
|
||||
</p>
|
||||
<?php if (!empty($user['comment'])): ?>
|
||||
<p class="user-comment"><?= htmlspecialchars($user['comment']) ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="user-actions">
|
||||
<button type="button" class="button edit-user" data-user-id="<?= htmlspecialchars($user['id']) ?>" data-user-comment="<?= htmlspecialchars($user['comment'] ?? '') ?>" data-user-role="<?= htmlspecialchars($user['role'] ?? 'editor') ?>">Modifier</button>
|
||||
<?php if ($index !== 0): ?>
|
||||
<button type="button" class="button delete-user" data-user-id="<?= htmlspecialchars($user['id']) ?>">Supprimer</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<button type="button" id="addUserBtn" class="button">Ajouter un utilisateur</button>
|
||||
</section>
|
||||
|
||||
<!-- Formulaire d'ajout d'utilisateur (modal) -->
|
||||
<div id="addUserModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>Ajouter un utilisateur</h2>
|
||||
</div>
|
||||
<form method="POST" id="addUserForm">
|
||||
<input type="hidden" name="action" value="add">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="user_id">Identifiant</label>
|
||||
<input type="text" id="user_id" name="user_id" required minlength="3">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Mot de passe</label>
|
||||
<input type="password" id="password" name="password" required minlength="8">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="confirm_password">Confirmation du mot de passe</label>
|
||||
<input type="password" id="confirm_password" name="confirm_password" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="role">Rôle</label>
|
||||
<select id="role" name="role">
|
||||
<option value="editor">Éditeur</option>
|
||||
<option value="admin">Administrateur</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="comment">Commentaire (optionnel)</label>
|
||||
<textarea id="comment" name="comment" rows="3"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="button dark cancel-btn">Annuler</button>
|
||||
<button type="submit" class="button">Ajouter</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Formulaire d'édition d'utilisateur (modal) -->
|
||||
<div id="editUserModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>Modifier un utilisateur</h2>
|
||||
</div>
|
||||
<form method="POST" id="editUserForm">
|
||||
<input type="hidden" name="action" value="edit">
|
||||
<input type="hidden" id="edit_user_id" name="user_id">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="new_user_id">Nouvel identifiant (laisser vide pour ne pas modifier)</label>
|
||||
<input type="text" id="new_user_id" name="new_user_id" minlength="3">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit_password">Nouveau mot de passe (laisser vide pour ne pas modifier)</label>
|
||||
<input type="password" id="edit_password" name="password" minlength="8">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit_role">Rôle</label>
|
||||
<select id="edit_role" name="role">
|
||||
<option value="editor">Éditeur</option>
|
||||
<option value="admin">Administrateur</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit_comment">Commentaire</label>
|
||||
<textarea id="edit_comment" name="comment" rows="3"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="button dark cancel-btn">Annuler</button>
|
||||
<button type="submit" class="button">Enregistrer</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Formulaire de suppression (caché) -->
|
||||
<form id="deleteUserForm" method="POST" style="display: none;">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" id="delete_user_id" name="user_id">
|
||||
</form>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Gestion des modales
|
||||
const addUserModal = document.getElementById('addUserModal');
|
||||
const editUserModal = document.getElementById('editUserModal');
|
||||
|
||||
// Boutons d'ouverture des modales
|
||||
const addUserBtn = document.getElementById('addUserBtn');
|
||||
|
||||
// Boutons de fermeture des modales
|
||||
const cancelBtns = document.querySelectorAll('.cancel-btn');
|
||||
|
||||
// Ouverture de la modale d'ajout
|
||||
addUserBtn.addEventListener('click', function() {
|
||||
addUserModal.style.display = 'block';
|
||||
document.getElementById('addUserForm').reset();
|
||||
});
|
||||
|
||||
// Ouverture de la modale d'édition
|
||||
document.querySelectorAll('.edit-user').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const userId = this.dataset.userId;
|
||||
const userComment = this.dataset.userComment;
|
||||
const userRole = this.dataset.userRole;
|
||||
|
||||
document.getElementById('edit_user_id').value = userId;
|
||||
document.getElementById('new_user_id').value = '';
|
||||
document.getElementById('edit_password').value = '';
|
||||
document.getElementById('edit_comment').value = userComment;
|
||||
|
||||
// Sélectionner la bonne option dans le select
|
||||
const roleSelect = document.getElementById('edit_role');
|
||||
for (let i = 0; i < roleSelect.options.length; i++) {
|
||||
if (roleSelect.options[i].value === userRole) {
|
||||
roleSelect.selectedIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Si c'est le premier utilisateur, désactiver le changement de rôle
|
||||
if (userId === '<?= htmlspecialchars(reset($config['users'])['id']) ?>') {
|
||||
roleSelect.disabled = true;
|
||||
} else {
|
||||
roleSelect.disabled = false;
|
||||
}
|
||||
|
||||
editUserModal.style.display = 'block';
|
||||
});
|
||||
});
|
||||
|
||||
// Fermeture des modales avec les boutons Annuler
|
||||
cancelBtns.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
addUserModal.style.display = 'none';
|
||||
editUserModal.style.display = 'none';
|
||||
});
|
||||
});
|
||||
|
||||
// Fermeture des modales en cliquant à l'extérieur
|
||||
window.addEventListener('click', function(event) {
|
||||
if (event.target === addUserModal) {
|
||||
addUserModal.style.display = 'none';
|
||||
} else if (event.target === editUserModal) {
|
||||
editUserModal.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Validation du formulaire d'ajout
|
||||
document.getElementById('addUserForm').addEventListener('submit', function(e) {
|
||||
const password = document.getElementById('password').value;
|
||||
const confirmPassword = document.getElementById('confirm_password').value;
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
e.preventDefault();
|
||||
alert('Les mots de passe ne correspondent pas');
|
||||
}
|
||||
});
|
||||
|
||||
// Suppression d'un utilisateur
|
||||
document.querySelectorAll('.delete-user').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const userId = this.dataset.userId;
|
||||
|
||||
confirmDialog.show({
|
||||
title: 'Suppression d\'utilisateur',
|
||||
message: `Voulez-vous vraiment supprimer l'utilisateur "${userId}" ? Cette action est irréversible.`,
|
||||
confirmText: 'Supprimer',
|
||||
confirmClass: 'danger',
|
||||
onConfirm: () => {
|
||||
document.getElementById('delete_user_id').value = userId;
|
||||
document.getElementById('deleteUserForm').submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<link rel="stylesheet" href="../assets/css/dialog.css">
|
||||
<script src="../assets/js/dialog.js"></script>
|
||||
|
||||
<style>
|
||||
/* Styles spécifiques à la page utilisateurs */
|
||||
.users-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
margin: var(--spacing-lg) 0;
|
||||
}
|
||||
|
||||
.user-item {
|
||||
background: var(--bg-tertiary);
|
||||
padding: var(--spacing-lg);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-item.admin-user {
|
||||
background: linear-gradient(135deg, var(--bg-tertiary), #57433a);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.user-item.admin-user::after {
|
||||
content: 'Admin principal';
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
background: var(--accent-primary);
|
||||
color: var(--text-tertiary);
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.user-info h3 {
|
||||
margin: 0 0 var(--spacing-xs) 0;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.user-role {
|
||||
color: var(--text-secondary);
|
||||
margin: 0 0 var(--spacing-xs) 0;
|
||||
}
|
||||
|
||||
.user-role span {
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.user-comment {
|
||||
font-style: italic;
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.user-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.user-actions .button {
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
}
|
||||
|
||||
.delete-user {
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.delete-user:hover {
|
||||
background-color: var(--error-color);
|
||||
}
|
||||
|
||||
/* Responsiveness */
|
||||
@media (max-width: 768px) {
|
||||
.user-item {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.user-actions {
|
||||
margin-top: var(--spacing-md);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 95%;
|
||||
max-width: 600px;
|
||||
height: auto;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
@ -21,6 +21,7 @@
|
||||
object-fit: cover;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.current-cover {
|
||||
@ -386,12 +387,41 @@
|
||||
|
||||
/* Media queries */
|
||||
@media (max-width: 768px) {
|
||||
.story-cover {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
max-width: none;
|
||||
.story-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.story-cover {
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
}
|
||||
|
||||
.story-info {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.story-actions {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
margin-top: var(--spacing-md);
|
||||
}
|
||||
|
||||
.story-actions .button {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
margin: 0 var(--spacing-xs);
|
||||
}
|
||||
|
||||
.story-actions .button:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.story-actions .button:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
.current-cover {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
@ -423,4 +453,34 @@
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
/* Adaptations pour très petits écrans */
|
||||
@media (max-width: 480px) {
|
||||
.story-actions {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.story-actions .button {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.story-item {
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.story-cover {
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
.story-info h2 {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.story-info p {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
@ -91,6 +91,58 @@
|
||||
background-color: var(--bg-primary);
|
||||
}
|
||||
|
||||
/* Styles pour la modale d'accès */
|
||||
.users-access-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
margin: var(--spacing-md) 0;
|
||||
padding: var(--spacing-md);
|
||||
background: var(--bg-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.user-access-item {
|
||||
padding: var(--spacing-sm);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.user-access-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.user-access-item label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.user-access-item input[type="checkbox"] {
|
||||
margin-right: var(--spacing-md);
|
||||
}
|
||||
|
||||
.user-role-badge {
|
||||
margin-left: var(--spacing-md);
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.user-role-badge.admin {
|
||||
background-color: var(--accent-primary);
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.user-role-badge.editor {
|
||||
background-color: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 0 var(--spacing-lg);
|
||||
overflow-y: auto;
|
||||
max-height: 60vh;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.confirm-dialog-content {
|
||||
width: calc(100% - 32px);
|
||||
|
@ -42,6 +42,17 @@
|
||||
border-color: var(--error-color);
|
||||
}
|
||||
|
||||
/* Menu hamburger pour mobile */
|
||||
.menu-toggle {
|
||||
display: none;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-primary);
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
padding: var(--spacing-sm);
|
||||
}
|
||||
|
||||
/* Conteneur principal */
|
||||
.admin-main {
|
||||
padding: var(--spacing-xl);
|
||||
@ -67,4 +78,39 @@
|
||||
.stories-list {
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
background-color: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: var(--spacing-md);
|
||||
z-index: 100;
|
||||
width: 200px;
|
||||
gap: var(--spacing-sm);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||
|
||||
/* État caché par défaut */
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(-10px);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.nav-menu.active {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.admin-nav {
|
||||
position: relative;
|
||||
}
|
||||
}
|
@ -1,4 +1,165 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Fonction de notification
|
||||
function showNotification(message, type = 'success') {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification ${type}`;
|
||||
notification.textContent = message;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// Animation d'entrée
|
||||
setTimeout(() => {
|
||||
notification.style.opacity = '1';
|
||||
notification.style.transform = 'translateY(0)';
|
||||
}, 10);
|
||||
|
||||
// Auto-suppression après 3 secondes
|
||||
setTimeout(() => {
|
||||
notification.style.opacity = '0';
|
||||
notification.style.transform = 'translateY(-100%)';
|
||||
setTimeout(() => notification.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Gestion de la modale d'accès
|
||||
const accessModal = document.getElementById('accessModal');
|
||||
const cancelAccessBtn = document.getElementById('cancelAccess');
|
||||
const saveAccessBtn = document.getElementById('saveAccess');
|
||||
const modalStoryTitle = document.getElementById('modalStoryTitle');
|
||||
let currentStoryId = null;
|
||||
|
||||
// Ouverture de la modale d'accès
|
||||
document.querySelectorAll('.manage-access').forEach(button => {
|
||||
button.addEventListener('click', async function() {
|
||||
currentStoryId = this.dataset.id;
|
||||
const storyTitle = this.closest('.story-item').querySelector('h2').textContent;
|
||||
modalStoryTitle.textContent = storyTitle;
|
||||
|
||||
// Réinitialiser les checkboxes
|
||||
document.querySelectorAll('input[name="user_access[]"]').forEach(checkbox => {
|
||||
if (!checkbox.disabled) {
|
||||
checkbox.checked = false;
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
// Charger les autorisations actuelles
|
||||
const response = await fetch(`story-edit.php?id=${currentStoryId}`);
|
||||
const data = await response.text();
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(data, 'text/html');
|
||||
|
||||
// Extraire les données du script
|
||||
const scripts = doc.querySelectorAll('script');
|
||||
let accessData = [];
|
||||
|
||||
for (const script of scripts) {
|
||||
if (script.textContent.includes('storyAccess')) {
|
||||
const match = script.textContent.match(/storyAccess\s*=\s*(\[.*?\])/s);
|
||||
if (match && match[1]) {
|
||||
try {
|
||||
accessData = JSON.parse(match[1]);
|
||||
} catch (e) {
|
||||
console.error('Erreur lors du parsing des données d\'accès:', e);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mettre à jour les checkboxes
|
||||
accessData.forEach(userId => {
|
||||
const checkbox = document.querySelector(`input[name="user_access[]"][value="${userId}"]`);
|
||||
if (checkbox && !checkbox.disabled) {
|
||||
checkbox.checked = true;
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des autorisations:', error);
|
||||
}
|
||||
|
||||
accessModal.style.display = 'block';
|
||||
});
|
||||
});
|
||||
|
||||
// Fermeture de la modale d'accès
|
||||
cancelAccessBtn.addEventListener('click', function() {
|
||||
accessModal.style.display = 'none';
|
||||
});
|
||||
|
||||
// Fermeture de la modale en cliquant à l'extérieur
|
||||
window.addEventListener('click', function(event) {
|
||||
if (event.target === accessModal) {
|
||||
accessModal.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Sauvegarde des autorisations d'accès
|
||||
saveAccessBtn.addEventListener('click', async function() {
|
||||
if (!currentStoryId) return;
|
||||
|
||||
// Récupérer les utilisateurs sélectionnés
|
||||
const selectedUsers = Array.from(document.querySelectorAll('input[name="user_access[]"]:checked'))
|
||||
.map(checkbox => checkbox.value);
|
||||
|
||||
try {
|
||||
const response = await fetch('api/story-access.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
storyId: currentStoryId,
|
||||
access: selectedUsers
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Erreur lors de la mise à jour des autorisations');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
showNotification('Autorisations mises à jour avec succès');
|
||||
accessModal.style.display = 'none';
|
||||
} else {
|
||||
throw new Error(result.error || 'Erreur inconnue');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur:', error);
|
||||
showNotification(error.message, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// Gestion du menu mobile
|
||||
const navMenu = document.querySelector('.nav-menu');
|
||||
const navBrand = document.querySelector('.nav-brand');
|
||||
|
||||
// Créer le bouton hamburger s'il n'existe pas déjà
|
||||
if (!document.querySelector('.menu-toggle')) {
|
||||
const menuToggle = document.createElement('button');
|
||||
menuToggle.className = 'menu-toggle';
|
||||
menuToggle.innerHTML = '☰';
|
||||
menuToggle.setAttribute('aria-label', 'Menu');
|
||||
|
||||
// Insérer le bouton avant le menu
|
||||
navMenu.parentNode.insertBefore(menuToggle, navMenu);
|
||||
|
||||
// Gérer les clics sur le bouton
|
||||
menuToggle.addEventListener('click', function() {
|
||||
navMenu.classList.toggle('active');
|
||||
});
|
||||
|
||||
// Fermer le menu au clic en dehors
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!navMenu.contains(e.target) && !menuToggle.contains(e.target)) {
|
||||
navMenu.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Gestion de la suppression des romans
|
||||
const storyList = document.querySelector('.stories-list');
|
||||
if (storyList) {
|
||||
|
86
chapitre.php
86
chapitre.php
@ -19,14 +19,20 @@ if (!$story) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification des droits d'accès
|
||||
$canViewDrafts = Auth::check() && Auth::canAccessStory($storyId);
|
||||
|
||||
// Recherche du chapitre
|
||||
$currentChapter = null;
|
||||
$currentIndex = -1;
|
||||
foreach ($story['chapters'] as $index => $chapter) {
|
||||
if (($chapter['draft'] ?? false) && !Auth::check()) {
|
||||
continue;
|
||||
}
|
||||
if ($chapter['id'] === $chapterId) {
|
||||
// Vérifier si le chapitre est en brouillon et si l'utilisateur a les droits
|
||||
if (($chapter['draft'] ?? false) && !$canViewDrafts) {
|
||||
// Rediriger vers la page du roman si l'utilisateur n'a pas accès aux brouillons
|
||||
header('Location: roman.php?id=' . urlencode($storyId));
|
||||
exit;
|
||||
}
|
||||
$currentChapter = $chapter;
|
||||
$currentIndex = $index;
|
||||
break;
|
||||
@ -38,9 +44,9 @@ if (!$currentChapter) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Récupération des chapitres précédent et suivant
|
||||
$visibleChapters = array_filter($story['chapters'], function($ch) {
|
||||
return !($ch['draft'] ?? false) || Auth::check();
|
||||
// Récupération des chapitres précédent et suivant (uniquement publiés ou brouillons si autorisés)
|
||||
$visibleChapters = array_filter($story['chapters'], function($ch) use ($canViewDrafts) {
|
||||
return !($ch['draft'] ?? false) || $canViewDrafts;
|
||||
});
|
||||
$visibleChapters = array_values($visibleChapters);
|
||||
|
||||
@ -68,7 +74,7 @@ $config = Config::load();
|
||||
</head>
|
||||
<body>
|
||||
<!-- En-tête avec image de couverture si disponible -->
|
||||
<header class="novel-header">
|
||||
<header class="novel-header <?= ($currentChapter['draft'] ?? false) ? 'draft-header' : '' ?>">
|
||||
<?php if (!empty($currentChapter['cover'])): ?>
|
||||
<div class="novel-header-background" style="background-image: url('<?= htmlspecialchars($currentChapter['cover']) ?>');"></div>
|
||||
<?php endif; ?>
|
||||
@ -76,7 +82,12 @@ $config = Config::load();
|
||||
<a href="index.php" class="about-button">Accueil</a>
|
||||
<a href="roman.php?id=<?= urlencode($storyId) ?>" class="about-button">Roman</a>
|
||||
</div>
|
||||
<h1><?= htmlspecialchars($currentChapter['title']) ?></h1>
|
||||
<h1>
|
||||
<?= htmlspecialchars($currentChapter['title']) ?>
|
||||
<?php if (($currentChapter['draft'] ?? false) && $canViewDrafts): ?>
|
||||
<span class="draft-badge">BROUILLON</span>
|
||||
<?php endif; ?>
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<!-- Contenu principal -->
|
||||
@ -106,16 +117,16 @@ $config = Config::load();
|
||||
<h2>Chapitres</h2>
|
||||
<ul class="chapters-list">
|
||||
<?php
|
||||
$visibleChapters = array_filter($story['chapters'], function($chapter) {
|
||||
return !($chapter['draft'] ?? false) || Auth::check();
|
||||
});
|
||||
|
||||
foreach ($visibleChapters as $chapter):
|
||||
$isDraft = $chapter['draft'] ?? false;
|
||||
?>
|
||||
<li>
|
||||
<a href="?story=<?= urlencode($storyId) ?>&chapter=<?= urlencode($chapter['id']) ?>"
|
||||
class="<?= $chapter['id'] === $chapterId ? 'current-chapter' : '' ?>">
|
||||
class="<?= $chapter['id'] === $chapterId ? 'current-chapter' : '' ?> <?= $isDraft ? 'draft-chapter' : '' ?>">
|
||||
<?= htmlspecialchars($chapter['title']) ?>
|
||||
<?php if ($isDraft && $canViewDrafts): ?>
|
||||
<span class="draft-label">(Brouillon)</span>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
@ -125,6 +136,55 @@ $config = Config::load();
|
||||
|
||||
<button class="scroll-top" aria-label="Retour en haut de page">↑</button>
|
||||
|
||||
<style>
|
||||
.draft-header::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: repeating-linear-gradient(
|
||||
45deg,
|
||||
rgba(210, 166, 121, 0.3),
|
||||
rgba(210, 166, 121, 0.3) 10px,
|
||||
rgba(0, 0, 0, 0) 10px,
|
||||
rgba(0, 0, 0, 0) 20px
|
||||
);
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.draft-badge {
|
||||
display: inline-block;
|
||||
background-color: var(--accent-primary);
|
||||
color: var(--text-tertiary);
|
||||
font-size: 0.5em;
|
||||
padding: 5px 10px;
|
||||
border-radius: 20px;
|
||||
vertical-align: middle;
|
||||
margin-left: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.draft-chapter {
|
||||
opacity: 0.7;
|
||||
border-left: 3px solid var(--accent-primary);
|
||||
padding-left: 8px !important;
|
||||
}
|
||||
|
||||
.draft-label {
|
||||
font-size: 0.8em;
|
||||
background-color: var(--accent-primary);
|
||||
color: var(--text-tertiary);
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
margin-left: 8px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const scrollTopBtn = document.querySelector('.scroll-top');
|
||||
|
@ -19,7 +19,8 @@
|
||||
{
|
||||
"id": "admin",
|
||||
"password": "$2y$10$pfYTH9z.ZvdgTJTj779X7.wL6m8S4.vSznQEiPdy6coaz.MeJkT76",
|
||||
"comment": "Mot de passe par défaut « admin ». À changer dès la première connexion."
|
||||
"role": "admin",
|
||||
"comment": "Compte administrateur principal."
|
||||
}
|
||||
]
|
||||
}
|
@ -35,4 +35,110 @@ class Auth {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si l'utilisateur actuel est l'administrateur principal
|
||||
* (premier utilisateur dans la liste)
|
||||
*/
|
||||
public static function isAdmin() {
|
||||
if (!self::check()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$config = Config::load();
|
||||
$firstUser = reset($config['users']);
|
||||
|
||||
return $_SESSION['user_id'] === $firstUser['id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si l'utilisateur a le rôle "admin"
|
||||
*/
|
||||
public static function hasAdminRole() {
|
||||
if (!self::check()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$config = Config::load();
|
||||
$userId = $_SESSION['user_id'];
|
||||
|
||||
foreach ($config['users'] as $user) {
|
||||
if ($user['id'] === $userId) {
|
||||
return isset($user['role']) && $user['role'] === 'admin';
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si l'utilisateur actuel a accès à un roman
|
||||
*/
|
||||
public static function canAccessStory($storyId) {
|
||||
if (!self::check()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$userId = $_SESSION['user_id'];
|
||||
$config = Config::load();
|
||||
|
||||
// L'administrateur principal a accès à tous les romans
|
||||
if ($userId === reset($config['users'])['id']) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Vérifier l'accès dans les paramètres du roman
|
||||
$story = Stories::get($storyId);
|
||||
if (!$story) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Si aucune restriction d'accès n'est définie, tous les utilisateurs ont accès
|
||||
if (!isset($story['access']) || empty($story['access'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Sinon, vérifier si l'utilisateur est dans la liste d'accès
|
||||
return in_array($userId, $story['access']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère le rôle de l'utilisateur actuel
|
||||
*/
|
||||
public static function getCurrentUserRole() {
|
||||
if (!self::check()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$config = Config::load();
|
||||
$userId = $_SESSION['user_id'];
|
||||
|
||||
foreach ($config['users'] as $user) {
|
||||
if ($user['id'] === $userId) {
|
||||
return $user['role'] ?? 'editor';
|
||||
}
|
||||
}
|
||||
|
||||
return 'editor';
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère la liste des utilisateurs (sauf l'utilisateur courant)
|
||||
*/
|
||||
public static function getAllUsers($excludeCurrentUser = true) {
|
||||
$config = Config::load();
|
||||
$users = [];
|
||||
|
||||
foreach ($config['users'] as $user) {
|
||||
if (!$excludeCurrentUser || $user['id'] !== ($_SESSION['user_id'] ?? null)) {
|
||||
$users[] = [
|
||||
'id' => $user['id'],
|
||||
'role' => $user['role'] ?? 'editor',
|
||||
'isAdmin' => $user === reset($config['users'])
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $users;
|
||||
}
|
||||
}
|
39
index.php
39
index.php
@ -72,6 +72,22 @@ function formatDate($date) {
|
||||
<main class="main-content">
|
||||
<div class="novels-grid">
|
||||
<?php foreach ($stories as $story): ?>
|
||||
<?php
|
||||
// Compter les chapitres visibles pour cet utilisateur
|
||||
$visibleChapters = array_filter($story['chapters'] ?? [], function($chapter) use ($story) {
|
||||
// Un chapitre est visible si:
|
||||
// - Il n'est pas en mode brouillon, OU
|
||||
// - L'utilisateur est connecté et a accès au roman
|
||||
return !($chapter['draft'] ?? false) || (Auth::check() && Auth::canAccessStory($story['id']));
|
||||
});
|
||||
|
||||
$chapterCount = count($visibleChapters);
|
||||
|
||||
// Ne pas afficher le roman s'il n'a aucun chapitre visible et que l'utilisateur n'a pas accès
|
||||
if ($chapterCount === 0 && !Auth::check()) {
|
||||
continue;
|
||||
}
|
||||
?>
|
||||
<a href="roman.php?id=<?= htmlspecialchars($story['id']) ?>" class="novel-card <?= isNewRelease($story) ? 'new-release' : '' ?>">
|
||||
<img src="<?= htmlspecialchars($story['cover']) ?>"
|
||||
alt="Couverture de <?= htmlspecialchars($story['title']) ?>"
|
||||
@ -80,15 +96,19 @@ function formatDate($date) {
|
||||
|
||||
<div class="novel-info">
|
||||
<h2><?= htmlspecialchars($story['title']) ?></h2>
|
||||
<?php
|
||||
$visibleChapters = array_filter($story['chapters'] ?? [], function($chapter) {
|
||||
return !($chapter['draft'] ?? false) || Auth::check();
|
||||
});
|
||||
$chapterCount = count($visibleChapters);
|
||||
?>
|
||||
<p>
|
||||
<?= $chapterCount ?>
|
||||
chapitre<?= $chapterCount > 1 ? 's' : '' ?>
|
||||
<?php if (Auth::check() && Auth::canAccessStory($story['id'])): ?>
|
||||
<?php
|
||||
$draftCount = count(array_filter($story['chapters'] ?? [], function($ch) {
|
||||
return $ch['draft'] ?? false;
|
||||
}));
|
||||
if ($draftCount > 0):
|
||||
?>
|
||||
<span class="draft-count">(dont <?= $draftCount ?> brouillon<?= $draftCount > 1 ? 's' : '' ?>)</span>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
<div class="novel-date">
|
||||
Mis à jour le <?= formatDate($story['updated']) ?>
|
||||
@ -101,6 +121,13 @@ function formatDate($date) {
|
||||
|
||||
<button class="scroll-top" aria-label="Retour en haut de page">↑</button>
|
||||
|
||||
<style>
|
||||
.draft-count {
|
||||
color: var(--accent-primary);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const scrollTopBtn = document.querySelector('.scroll-top');
|
||||
|
44
roman.php
44
roman.php
@ -18,6 +18,9 @@ if (!$story) {
|
||||
}
|
||||
|
||||
$config = Config::load();
|
||||
|
||||
// Vérification des droits d'accès pour voir les chapitres en brouillon
|
||||
$canViewDrafts = Auth::check() && Auth::canAccessStory($storyId);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
@ -58,18 +61,30 @@ $config = Config::load();
|
||||
<?php if (!empty($story['chapters'])): ?>
|
||||
<ul class="chapters-list">
|
||||
<?php
|
||||
$visibleChapters = array_filter($story['chapters'], function($chapter) {
|
||||
return !($chapter['draft'] ?? false) || Auth::check();
|
||||
// Filtrer les chapitres pour n'afficher que les chapitres publiés
|
||||
// ou les brouillons si l'utilisateur a les droits
|
||||
$visibleChapters = array_filter($story['chapters'], function($chapter) use ($canViewDrafts) {
|
||||
return !($chapter['draft'] ?? false) || $canViewDrafts;
|
||||
});
|
||||
|
||||
foreach ($visibleChapters as $chapter):
|
||||
if (empty($visibleChapters)): ?>
|
||||
<p>Aucun chapitre publié disponible pour le moment.</p>
|
||||
<?php else:
|
||||
foreach ($visibleChapters as $chapter):
|
||||
?>
|
||||
<li>
|
||||
<a href="chapitre.php?story=<?= urlencode($story['id']) ?>&chapter=<?= urlencode($chapter['id']) ?>">
|
||||
<a href="chapitre.php?story=<?= urlencode($story['id']) ?>&chapter=<?= urlencode($chapter['id']) ?>"
|
||||
class="<?= ($chapter['draft'] ?? false) ? 'draft-chapter' : '' ?>">
|
||||
<?= htmlspecialchars($chapter['title']) ?>
|
||||
<?php if (($chapter['draft'] ?? false) && $canViewDrafts): ?>
|
||||
<span class="draft-label">(Brouillon)</span>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
<?php
|
||||
endforeach;
|
||||
endif;
|
||||
?>
|
||||
</ul>
|
||||
<?php else: ?>
|
||||
<p>Aucun chapitre disponible pour le moment.</p>
|
||||
@ -79,6 +94,25 @@ $config = Config::load();
|
||||
|
||||
<button class="scroll-top" aria-label="Retour en haut de page">↑</button>
|
||||
|
||||
<style>
|
||||
.draft-chapter {
|
||||
opacity: 0.7;
|
||||
border-left: 3px solid var(--accent-primary);
|
||||
padding-left: 8px !important;
|
||||
}
|
||||
|
||||
.draft-label {
|
||||
font-size: 0.8em;
|
||||
background-color: var(--accent-primary);
|
||||
color: var(--text-tertiary);
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
margin-left: 8px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const scrollTopBtn = document.querySelector('.scroll-top');
|
||||
|
@ -1 +1 @@
|
||||
1.2.0
|
||||
1.3.0
|
Loading…
x
Reference in New Issue
Block a user