mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-06 19:19:19 +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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user