diff --git a/TODO.md b/TODO.md index 379b2ab..42fd016 100644 --- a/TODO.md +++ b/TODO.md @@ -1,33 +1,7 @@ # TODO -- [x] Make thanks.php respect student mode (no header, centered "add new form" button) - - [x] Add hidden input `student_mode` in add.php form when in student mode - - [x] Append `mode=student` to thanks redirect in formulaire.php - - [x] Update thanks.php to detect student mode, hide header, show centered button -- [x] Cleanup public/admin/add.php — standardise fieldsets and add licence explanation sections from docs PDF - - [x] Organise all fields into `
/` blocks: Informations du TFE, Composition du jury, Cadre académique, Fichiers, Métadonnées complémentaires - - [x] Remove double-wrapping of jury-fieldset (it has its own `
`) - - [x] Add "Degrés d'ouverture et licences" section (Libre / Interne / Interdit + Généralités) wrapped in `if ($studentMode)` — hidden in admin - -- [x] Migrate student mode form to shareable links system (/partage/) - - [x] Create `share_links` database table (id, slug YYYYMMDD-random, password_hash, is_active, usage_count, created_by, created_at, expires_at nullable) - - [x] Create `ShareLink` model — generate slugs, validate, verify password, CRUD - - [x] Create `public/partage/index.php` — public form page (no auth required, validates link active + password if set) - - [x] Create `public/partage/.htaccess` — RewriteRule to route all partage paths to index.php - - [x] Create `public/partage/thanks.php` — post-submission confirmation page - - [x] Move student-specific licence explanation fieldset to partage form template - - [x] Share-link specific CSRF token (session-scoped `share_csrf_`) instead of session CSRF - -- [x] Create admin page for managing student access links - - [x] Create `public/admin/student-access.php` — "Accès étudiant·e" page - - [x] Link to new page from admin navigation - - [x] Implement list view of all share links with status (active/disabled, password set, usage count, created date) - - [x] Implement create new link modal/form (optional expiration, password) - - [x] Implement toggle active/disabled status per link - - [x] Implement password set/change/clear per link - - [x] Implement delete link action - - [x] Copy-to-clipboard button for full partage URL - -- [x] Security and validation considerations - - [x] Rate limiting on form submissions per share link — integrate RateLimit into partage index.php POST handler - - [x] Add flash messages / error handling for invalid/disabled/password-protected links — replace plain die() with styled error pages and flash messages +## Completed +- [x] Fix share link slug regex mismatch (base64 chars vs base32 pattern) +- [x] Fix regex delimiter clash (`/` inside `[...]` broke the pattern) → switched to `#` delimiter +- [x] Add PHP dev server router for /partage/ URL rewriting +- [x] Add nginx location block for /partage/ pretty URLs diff --git a/config/router.php b/config/router.php new file mode 100644 index 0000000..148ace7 --- /dev/null +++ b/config/router.php @@ -0,0 +1,26 @@ + to public/partage/index.php, since the built-in + * server has no URL rewriting like nginx's try_files. + */ + +$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); + +// Route /partage/ and /partage// to the partage entry +if (preg_match('#^/partage(/.*)?$#', $uri)) { + $_SERVER['SCRIPT_NAME'] = '/partage/index.php'; + require __DIR__ . '/../public/partage/index.php'; + return true; +} + +// Route /tfe/<...> to tfe.php +if (preg_match('#^/tfe(/.*)?$#', $uri)) { + $_SERVER['SCRIPT_NAME'] = '/tfe.php'; + require __DIR__ . '/../public/tfe.php'; + return true; +} + +// Default: serve static files if they exist +return false; diff --git a/justfile b/justfile index 97172ab..227f8c3 100644 --- a/justfile +++ b/justfile @@ -13,7 +13,7 @@ setup: [group('dev')] serve: migrate - @php -S 127.0.0.1:8000 -t public/ 2>&1 | stdbuf -oL grep -E '(Development Server|\[200\])' | stdbuf -oL grep -v 'live-reload\.php' || true + @php -S 127.0.0.1:8000 -t public/ config/router.php 2>&1 | stdbuf -oL grep -E '(Development Server|\[200\])' | stdbuf -oL grep -v 'live-reload\.php' || true [group('dev')] stop: diff --git a/nginx/posterg.conf b/nginx/posterg.conf index bcdc030..899be36 100644 --- a/nginx/posterg.conf +++ b/nginx/posterg.conf @@ -151,6 +151,11 @@ server { try_files $uri $uri/ =404; } + # Share-link (partage) — rewrite pretty URLs to index.php + location /partage/ { + try_files $uri /partage/index.php$is_args$args; + } + # Search endpoint - rate limiting location = /search.php { limit_req zone=search burst=10 nodelay; diff --git a/public/admin/acces-etudiante.php b/public/admin/acces-etudiante.php new file mode 100644 index 0000000..af4c91a --- /dev/null +++ b/public/admin/acces-etudiante.php @@ -0,0 +1,201 @@ +listAll(); + +$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; +$baseUrl = $protocol . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost'); +$pageTitle = 'Accès étudiant·e'; +$isAdmin = true; +$bodyClass = 'admin-body'; +?> + + + +
+ + +
+

Accès étudiant·e

+
+ +
+
+ + +

Aucun lien d'accès créé. Cliquez sur « Créer un lien » pour générer un lien partageable.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
LienStatutMot de passeUtilisationsExpirationCréé leActions
+ + + + + + + + + + + +
+ +
+ + + + +
+ +
+ + + + +
+
+
+ +
+ + + +
+

Créer un lien d'accès

+ +
+
+ + +
+ + + Laissez vide pour un lien sans mot de passe. +
+
+ + + Laissez vide pour qu'il n'expire jamais. +
+ +
+
+ + + +
+

Mot de passe

+ +
+
+ + + +
+ + + Laissez vide pour supprimer le mot de passe. +

+
+ +
+
+ + + + diff --git a/public/admin/actions/student-access.php b/public/admin/actions/acces-etudiante.php similarity index 67% rename from public/admin/actions/student-access.php rename to public/admin/actions/acces-etudiante.php index a6095e4..0323ff3 100644 --- a/public/admin/actions/student-access.php +++ b/public/admin/actions/acces-etudiante.php @@ -28,19 +28,19 @@ switch ($action) { // datetime-local gives "YYYY-MM-DDTHH:MM" $expiresAt = date('Y-m-d H:i:s', strtotime($expiresRaw)); if ($expiresAt <= date('Y-m-d H:i:s')) { - App::redirect('/admin/student-access.php', error: "La date d'expiration doit être dans le futur."); + App::redirect('/admin/acces-etudiante.php', error: "La date d'expiration doit être dans le futur."); } } $shareLink->create(1, $password, $expiresAt); - App::redirect('/admin/student-access.php', success: 'Lien d\'accès créé.'); + App::redirect('/admin/acces-etudiante.php', success: 'Lien d\'accès créé.'); break; case 'toggle': if ($id > 0) { $shareLink->toggleActive($id); - App::redirect('/admin/student-access.php', success: 'Statut du lien modifié.'); + App::redirect('/admin/acces-etudiante.php', success: 'Statut du lien modifié.'); } else { - App::redirect('/admin/student-access.php', error: 'Lien introuvable.'); + App::redirect('/admin/acces-etudiante.php', error: 'Lien introuvable.'); } break; @@ -48,22 +48,22 @@ switch ($action) { if ($id > 0) { $password = isset($_POST['password']) && $_POST['password'] !== '' ? trim($_POST['password']) : null; $shareLink->setPassword($id, $password); - App::redirect('/admin/student-access.php', success: 'Mot de passe mis à jour.'); + App::redirect('/admin/acces-etudiante.php', success: 'Mot de passe mis à jour.'); } else { - App::redirect('/admin/student-access.php', error: 'Lien introuvable.'); + App::redirect('/admin/acces-etudiante.php', error: 'Lien introuvable.'); } break; case 'delete': if ($id > 0) { $shareLink->delete($id); - App::redirect('/admin/student-access.php', success: 'Lien supprimé.'); + App::redirect('/admin/acces-etudiante.php', success: 'Lien supprimé.'); } else { - App::redirect('/admin/student-access.php', error: 'Lien introuvable.'); + App::redirect('/admin/acces-etudiante.php', error: 'Lien introuvable.'); } break; default: - App::redirect('/admin/student-access.php', error: 'Action inconnue.'); + App::redirect('/admin/acces-etudiante.php', error: 'Action inconnue.'); break; } diff --git a/public/admin/student-access.php b/public/admin/student-access.php deleted file mode 100644 index 8196418..0000000 --- a/public/admin/student-access.php +++ /dev/null @@ -1,244 +0,0 @@ -listAll(); -$flash = App::consumeFlash(); - -$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; -$baseUrl = $protocol . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost'); -$pageTitle = 'Accès étudiant·e'; -$isAdmin = true; -$bodyClass = 'admin-body'; -?> - - - - - -
- -
- - -
- - -
-

Accès étudiant·e

- -
- - -
- -

Aucun lien d'accès créé.

-

Cliquez sur « Créer un lien » pour générer un lien partageable.

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
LienStatutMot de passeUtilisationsExpirationCréé leActions
- - - -
- -
- - - - -
- -
- - - - -
-
-
- -
- - - -
-

Créer un lien d'accès

- -
-
- - -
- - -
- -
-
- - - -
-

Mot de passe

- -
-
- - - -
- -

-
- -
-
- - - - diff --git a/public/partage/index.php b/public/partage/index.php index 8ff726b..ca882fa 100644 --- a/public/partage/index.php +++ b/public/partage/index.php @@ -22,7 +22,7 @@ $slug = $parts[0] ?? ''; $action = $parts[1] ?? ''; // Validate slug format: YYYYMMDD-XXXXXXXX (17 chars) -if (!preg_match('/^\d{8}-[A-Z2-7]{8}$/', $slug)) { +if (!preg_match('#^\d{8}-[A-Z0-9+/]{8}$#', $slug)) { App::boot(); $_SESSION['_flash_error'] = 'Ce lien de partage n\'est pas valide.'; header('Location: /'); diff --git a/src/ShareLink.php b/src/ShareLink.php index ad41abe..f7c33b6 100644 --- a/src/ShareLink.php +++ b/src/ShareLink.php @@ -30,7 +30,11 @@ class ShareLink public static function generateSlug(): string { $date = date('Ymd'); - $random = substr(strtoupper(rtrim(base64_encode(random_bytes(7)), '=')), 0, 8); + $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; + $random = ''; + for ($i = 0; $i < 8; $i++) { + $random .= $chars[random_int(0, strlen($chars) - 1)]; + } return $date . '-' . $random; } diff --git a/storage/posterg.db b/storage/posterg.db index ef76e47..e9afeee 100644 Binary files a/storage/posterg.db and b/storage/posterg.db differ diff --git a/templates/header.php b/templates/header.php index 2694ae9..3a00a5b 100644 --- a/templates/header.php +++ b/templates/header.php @@ -21,7 +21,7 @@ $_thesisId = $_GET['id'] ?? null;
  • >Pages statiques
  • >Mots-clés
  • >Système
  • -
  • >Accès étudiant·e
  • +
  • >Accès étudiant·e
  • >Paramètres
  • >Modifier