mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-06 11:09:18 +02:00
Implement TFE file access restriction feature (complete)
Requirements: - parametres.php toggle: 'restricted_files_enabled' enables/disables the feature - Public TFE page: when enabled + access_type=Interne, hides files, shows French restriction message + access request form (metadata/synopsis still visible) - ERG emails (@erg.school / @erg.be): auto-approve, send 24h access link immediately - External emails: show justification textarea, create pending request, notify admin - Admin panel /admin/file-access.php: approve/reject requests with optional notes, sends access email on approval (linked from admin nav with pending count badge) Security: - One-time 24h email tokens (used_at + is_valid=0 on first click) - Token redeemed via POST /validate-access (GET shows confirmation page only) - Long-lived 30-day browser session in file_access_sessions table - Cookie: HttpOnly + Secure + SameSite=Strict - CSRF on all mutations, rate limiting on request submission - Audit trail: IP, UA, event, timestamp in file_access_audit Bug fixes: - admin/file-access.php: $vars never extract()ed → page was blank - Template had self-contained head/footer includes (double-include) - Admin approval URL used $requestId instead of $request['thesis_id'] - App::boot() now starts session so CSRF token works on public pages - Dispatcher routes /validate-access and /request-access through front controller
This commit is contained in:
199
app/templates/admin/file-access.php
Normal file
199
app/templates/admin/file-access.php
Normal file
@@ -0,0 +1,199 @@
|
||||
<main id="main-content">
|
||||
<h1>Demandes d'accès aux fichiers</h1>
|
||||
|
||||
<div class="access-req-stats">
|
||||
<div class="access-req-stat-card">
|
||||
<span class="access-req-stat-number"><?= $pendingCount ?></span>
|
||||
<span class="access-req-stat-label">En attente</span>
|
||||
</div>
|
||||
<div class="access-req-stat-card">
|
||||
<span class="access-req-stat-number"><?= $approvedCount ?></span>
|
||||
<span class="access-req-stat-label">Approuvées</span>
|
||||
</div>
|
||||
<div class="access-req-stat-card">
|
||||
<span class="access-req-stat-number"><?= $rejectedCount ?></span>
|
||||
<span class="access-req-stat-label">Rejetées</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="access-req-tabs">
|
||||
<a href="?status=pending" class="access-req-tab <?= $status === 'pending' ? 'active' : '' ?>">
|
||||
En attente <?= $pendingCount > 0 ? "({$pendingCount})" : '' ?>
|
||||
</a>
|
||||
<a href="?status=approved" class="access-req-tab <?= $status === 'approved' ? 'active' : '' ?>">
|
||||
Approuvées
|
||||
</a>
|
||||
<a href="?status=rejected" class="access-req-tab <?= $status === 'rejected' ? 'active' : '' ?>">
|
||||
Rejetées
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<?php if (empty($requests)): ?>
|
||||
<div class="access-req-empty">
|
||||
<p>Aucune demande <?= $status === 'pending' ? 'en attente' : ($status === 'approved' ? 'approuvée' : 'rejetée') ?>.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="access-req-list">
|
||||
<?php foreach ($requests as $req): ?>
|
||||
<div class="access-req-card">
|
||||
<div class="access-req-card__header">
|
||||
<div class="access-req-card__thesis">
|
||||
<h3><?= htmlspecialchars($req['title']) ?></h3>
|
||||
<p class="access-req-card__authors">
|
||||
<?php if (!empty($req['authors'])): ?>
|
||||
par <?= htmlspecialchars($req['authors']) ?>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($req['year'])): ?>
|
||||
— <?= htmlspecialchars($req['year']) ?>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
<div class="access-req-card__meta">
|
||||
<span class="access-req-badge access-req-badge--<?= $status ?>">
|
||||
<?= $status === 'pending' ? 'En attente' : ($status === 'approved' ? 'Approuvée' : 'Rejetée') ?>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="access-req-card__body">
|
||||
<div class="access-req-card__info">
|
||||
<div>
|
||||
<strong>Email :</strong>
|
||||
<a href="mailto:<?= htmlspecialchars($req['email']) ?>">
|
||||
<?= htmlspecialchars($req['email']) ?>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Date :</strong>
|
||||
<?= date('d/m/Y à H:i', strtotime($req['created_at'])) ?>
|
||||
</div>
|
||||
<?php if ($status === 'approved' && !empty($req['approved_at'])): ?>
|
||||
<div>
|
||||
<strong>Approuvée le :</strong>
|
||||
<?= date('d/m/Y à H:i', strtotime($req['approved_at'])) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($status === 'rejected' && !empty($req['approved_at'])): ?>
|
||||
<div>
|
||||
<strong>Rejetée le :</strong>
|
||||
<?= date('d/m/Y à H:i', strtotime($req['approved_at'])) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($req['justification'])): ?>
|
||||
<div class="access-req-card__justification">
|
||||
<strong>Justification :</strong>
|
||||
<p><?= nl2br(htmlspecialchars($req['justification'])) ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($status === 'rejected' && !empty($req['admin_notes'])): ?>
|
||||
<div class="access-req-card__admin-notes">
|
||||
<strong>Note de l'administrateur :</strong>
|
||||
<p><?= nl2br(htmlspecialchars($req['admin_notes'])) ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($status === 'pending'): ?>
|
||||
<div class="access-req-card__actions">
|
||||
<button type="button"
|
||||
class="access-req-btn access-req-btn--approve"
|
||||
onclick="openApproveDialog(<?= $req['id'] ?>)">
|
||||
Approuver
|
||||
</button>
|
||||
<button type="button"
|
||||
class="access-req-btn access-req-btn--reject"
|
||||
onclick="openRejectDialog(<?= $req['id'] ?>)">
|
||||
Rejeter
|
||||
</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($totalPages > 1): ?>
|
||||
<nav class="access-req-pagination">
|
||||
<?php if ($page > 1): ?>
|
||||
<a href="?status=<?= $status ?>&page=<?= $page - 1 ?>" class="access-req-pagination__link">
|
||||
← Précédent
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<span class="access-req-pagination__info">
|
||||
Page <?= $page ?> sur <?= $totalPages ?>
|
||||
</span>
|
||||
|
||||
<?php if ($page < $totalPages): ?>
|
||||
<a href="?status=<?= $status ?>&page=<?= $page + 1 ?>" class="access-req-pagination__link">
|
||||
Suivant →
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</main>
|
||||
|
||||
<!-- Approve Dialog -->
|
||||
<dialog id="approve-dialog" class="admin-dialog">
|
||||
<div class="admin-dialog__header">
|
||||
<h2>Approuver la demande</h2>
|
||||
<button type="button" class="admin-dialog__close"
|
||||
onclick="document.getElementById('approve-dialog').close()">×</button>
|
||||
</div>
|
||||
<form method="post" action="/admin/actions/access-request.php">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="request_id" id="approve-request-id">
|
||||
<input type="hidden" name="action" value="approve">
|
||||
|
||||
<label for="approve-notes">Note optionnelle (inclus dans l'email) :</label>
|
||||
<textarea name="admin_notes" id="approve-notes" rows="3"
|
||||
placeholder="Message personnalisé pour le demandeur..."></textarea>
|
||||
|
||||
<div class="admin-form-footer">
|
||||
<button type="submit" class="admin-btn">Approuver et envoyer email</button>
|
||||
<button type="button" class="admin-btn-secondary"
|
||||
onclick="document.getElementById('approve-dialog').close()">Annuler</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<!-- Reject Dialog -->
|
||||
<dialog id="reject-dialog" class="admin-dialog">
|
||||
<div class="admin-dialog__header">
|
||||
<h2>Rejeter la demande</h2>
|
||||
<button type="button" class="admin-dialog__close"
|
||||
onclick="document.getElementById('reject-dialog').close()">×</button>
|
||||
</div>
|
||||
<form method="post" action="/admin/actions/access-request.php">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="request_id" id="reject-request-id">
|
||||
<input type="hidden" name="action" value="reject">
|
||||
|
||||
<label for="reject-notes">Raison du rejet (optionnel) :</label>
|
||||
<textarea name="admin_notes" id="reject-notes" rows="3"
|
||||
placeholder="Raison du rejet..."></textarea>
|
||||
|
||||
<div class="admin-form-footer">
|
||||
<button type="submit" class="admin-btn admin-btn--danger">Rejeter</button>
|
||||
<button type="button" class="admin-btn-secondary"
|
||||
onclick="document.getElementById('reject-dialog').close()">Annuler</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<script>
|
||||
function openApproveDialog(requestId) {
|
||||
document.getElementById('approve-request-id').value = requestId;
|
||||
document.getElementById('approve-dialog').showModal();
|
||||
}
|
||||
|
||||
function openRejectDialog(requestId) {
|
||||
document.getElementById('reject-request-id').value = requestId;
|
||||
document.getElementById('reject-dialog').showModal();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -102,6 +102,19 @@
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Restriction d'accès aux fichiers</legend>
|
||||
|
||||
<label class="param-checkbox">
|
||||
<input type="checkbox" name="restricted_files_enabled" value="1"
|
||||
<?= ($siteSettings['restricted_files_enabled'] ?? '0') === '1' ? 'checked' : '' ?>>
|
||||
<span>
|
||||
<strong>Activer la restriction d'accès</strong><br>
|
||||
<small>Pour les TFE de type "Interne", masquer les fichiers et exiger une demande d'accès par email. Les métadonnées et le résumé restent visibles publiquement.</small>
|
||||
</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<button type="submit">Enregistrer</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
@@ -19,6 +19,12 @@ $_thesisId = $_GET['id'] ?? null;
|
||||
<li><a href="/admin/" <?= $_currentPage === 'index.php' ? 'aria-current="page"' : '' ?>>Liste des TFE</a></li>
|
||||
<li><a href="/admin/contenus.php" <?= in_array($_currentPage, ['contenus.php', 'contenus-edit.php']) ? 'aria-current="page"' : '' ?>>Contenus</a></li>
|
||||
<li><a href="/admin/tags.php" <?= $_currentPage === 'tags.php' ? 'aria-current="page"' : '' ?>>Mots-clés</a></li>
|
||||
<li><a href="/admin/file-access.php" <?= $_currentPage === 'file-access.php' ? 'aria-current="page"' : '' ?>>
|
||||
Demandes d'accès
|
||||
<?php if (isset($pendingCount) && $pendingCount > 0): ?>
|
||||
<span class="admin-nav-badge"><?= $pendingCount ?></span>
|
||||
<?php endif; ?>
|
||||
</a></li>
|
||||
<li><a href="/admin/system.php" <?= in_array($_currentPage, ['system.php', 'status.php', 'logs.php']) ? 'aria-current="page"' : '' ?>>Système</a></li>
|
||||
<li><a href="/admin/acces-etudiante.php" <?= $_currentPage === 'acces-etudiante.php' ? 'aria-current="page"' : '' ?>>Accès étudiant·e</a></li>
|
||||
<li><a href="/admin/parametres.php" <?= $_currentPage === 'parametres.php' ? 'aria-current="page"' : '' ?>>Paramètres</a></li>
|
||||
|
||||
@@ -193,6 +193,100 @@
|
||||
<p class="tfe-restricted">
|
||||
Ce TFE n'est pas disponible en ligne.
|
||||
</p>
|
||||
<?php elseif ($shouldHideFiles): ?>
|
||||
<div class="tfe-restricted-access">
|
||||
<p class="tfe-restricted-message">
|
||||
<strong>Accès restreint</strong><br>
|
||||
Les fichiers attachés à ce TFE sont réservés aux utilisateurs autorisés.
|
||||
</p>
|
||||
|
||||
<form id="access-request-form" class="tfe-access-request-form"
|
||||
data-thesis-id="<?= $thesisId ?>">
|
||||
<input type="hidden" name="csrf_token"
|
||||
value="<?= htmlspecialchars($_SESSION['csrf_token'] ?? '') ?>">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="access-email">Votre adresse email :</label>
|
||||
<input type="email"
|
||||
id="access-email"
|
||||
name="email"
|
||||
required
|
||||
placeholder="votre@email.com">
|
||||
</div>
|
||||
|
||||
<div id="justification-container" class="form-group" style="display: none;">
|
||||
<label for="access-justification">Pourquoi souhaitez-vous accéder à ce TFE ?</label>
|
||||
<textarea id="access-justification"
|
||||
name="justification"
|
||||
rows="4"
|
||||
placeholder="Décrivez brièvement votre motivation (recherche, collaboration, etc.)"></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="tfe-btn-request-access">
|
||||
Demander l'accès
|
||||
</button>
|
||||
|
||||
<div id="access-request-message" class="tfe-access-message" style="display: none;"></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const form = document.getElementById('access-request-form');
|
||||
const emailInput = document.getElementById('access-email');
|
||||
const justificationContainer = document.getElementById('justification-container');
|
||||
const justificationInput = document.getElementById('access-justification');
|
||||
const messageDiv = document.getElementById('access-request-message');
|
||||
|
||||
// Show/hide justification based on email domain
|
||||
emailInput.addEventListener('input', function() {
|
||||
const email = this.value.trim().toLowerCase();
|
||||
const isErg = email.endsWith('@erg.school') || email.endsWith('@erg.be');
|
||||
justificationContainer.style.display = isErg ? 'none' : 'block';
|
||||
justificationInput.required = !isErg;
|
||||
});
|
||||
|
||||
// Form submission
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = 'Envoi en cours...';
|
||||
messageDiv.style.display = 'none';
|
||||
|
||||
const formData = new FormData(form);
|
||||
formData.append('thesis_id', '<?= $thesisId ?>');
|
||||
|
||||
fetch('/request-access.php', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = 'Demander l\'accès';
|
||||
|
||||
messageDiv.style.display = 'block';
|
||||
if (data.success) {
|
||||
messageDiv.className = 'tfe-access-message tfe-access-success';
|
||||
messageDiv.textContent = data.message;
|
||||
form.reset();
|
||||
} else {
|
||||
messageDiv.className = 'tfe-access-message tfe-access-error';
|
||||
messageDiv.textContent = data.message || 'Une erreur est survenue. Veuillez réessayer.';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = 'Demander l\'accès';
|
||||
messageDiv.style.display = 'block';
|
||||
messageDiv.className = 'tfe-access-message tfe-access-error';
|
||||
messageDiv.textContent = 'Erreur de connexion. Veuillez réessayer.';
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<?php elseif (!empty($data['files'])): ?>
|
||||
<?php foreach ($data['files'] as $file): ?>
|
||||
<?php
|
||||
|
||||
Reference in New Issue
Block a user