From e9747edce044223102dcbc518a6d36d356568cbc Mon Sep 17 00:00:00 2001 From: Pontoporeia Date: Tue, 18 Aug 2026 19:28:16 +0200 Subject: [PATCH] docs: verify and refactor documentation to match current codebase --- .gitignore | 3 + README.md | 43 ++-- app/public/admin/README.md | 192 ++++++--------- app/storage/README.md | 221 ++--------------- docs/CSS.md | 40 ++-- docs/README.md | 77 ++++++ docs/bookmarklet.md | 168 +++++++------ docs/database.md | 418 ++++++++++++++------------------- docs/deployment.md | 309 ++++++++++++------------ docs/development.md | 340 +++++++++------------------ docs/export.md | 10 +- docs/file-uploads.md | 53 +++-- docs/import.md | 223 +++++++----------- docs/search.md | 210 ++++++----------- docs/security.md | 129 +++++----- nginx/README.md | 4 +- nginx/docs/SECURITY_HEADERS.md | 8 +- 17 files changed, 1006 insertions(+), 1442 deletions(-) create mode 100644 docs/README.md diff --git a/.gitignore b/.gitignore index ca408e7..5b67970 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,6 @@ app/public/assets/dist/ .phpunit.result.cache coverage/ + +# td (TODO manager) local database +.todo.sqlite diff --git a/README.md b/README.md index c732820..8033908 100644 --- a/README.md +++ b/README.md @@ -6,36 +6,26 @@ Répertoire des travaux de fin d'études de l'[ERG](https://erg.be) (École de R ## Requirements -- PHP 8.4 -- SQLite3 (`php8.4-sqlite3`) +- PHP ≥ 8.4 (with `ext-json`, `ext-openssl`, `ext-pdo`, `ext-sqlite3`) +- Composer +- Node.js / npm - nginx (production) ## Development - -### MacOS - -Logiciels: - -- un IDE pour éditer → VSCode -- git (ou une interface graphique) pour partager les modifications → git-gui (officiel) ou Github Desktop -- un server web avec PHP pour visualiser le project dans le navigateur → MAMP - - -### Workflow - -0. Faire un changement dans ton IDE -1. Démarrer le site via MAMP, en sélectionnant le dossier `public` -2. Vérifier que ça marche sur le site en local, depuis ton navigateur -3. Une fois qu'un changement spécifique est fait, `commit` les changements sur les fichiers qui sont relatif à ce changement -4. Vérifier que vous avez syncroniser avec le `remote` → `pull` + `rebase` ! pas merge -5. `push` les changements vers le remote - - ```bash -just serve # http://localhost:8000 (public) and /admin/ +# one-time +composer install +npm ci + +# run the dev server (builds assets, applies migrations, opens a browser) +just dev # http://127.0.0.1:8000 (public) and /admin/ +just stop # stop it ``` +Live CSS/JS rebuilds happen automatically via a chokidar watcher while `just dev` runs. +See [`docs/development.md`](docs/development.md) for the full workflow. + ## Deployment Files are pushed to the server with rsync — there is no repo on the remote. @@ -68,8 +58,11 @@ just deploy-nginx ## Security notes - Admin panel protected by PHP session (`AdminAuth`) — password-only, no username -- Uploads stored outside webroot, served via controlled `media.php` -- Rate limiting on public search (`src/RateLimit.php`) +- Uploads stored outside webroot, served via controlled `/media` (`MediaController`) +- Rate limiting on public search (`app/src/RateLimit.php`) - See `nginx/docs/SECURITY_HEADERS.md` for security headers reference +## More docs + +See [`docs/README.md`](docs/README.md) for the full documentation index. diff --git a/app/public/admin/README.md b/app/public/admin/README.md index 25707a8..224738f 100644 --- a/app/public/admin/README.md +++ b/app/public/admin/README.md @@ -1,145 +1,89 @@ # Admin Panel Structure -This directory contains the admin panel for managing XAMXAM thesis database. +This directory and `app/templates/admin/` contain the admin panel for managing +the XAMXAM TFE database. -## Directory Structure +## Entry points (`app/public/admin/`) -``` -public/admin/ -├── index.php # List all theses (main page) -├── add.php # Add new thesis form -├── edit.php # Edit existing thesis form -├── import.php # CSV import form -├── recapitulatif.php # Recap page after submission -├── actions/ # Backend processing scripts (no HTML output) -│ ├── formulaire.php # Process thesis submission from add.php -│ └── publish.php # Toggle publish/unpublish status -├── inc/ # Shared templates -│ ├── head.php # HTML head, CSS, navigation -│ └── footer.php # HTML footer -└── data/ # Upload directory (not in git) - ├── theses/ # PDF files - └── covers/ # Cover images -``` +| File | Purpose | +|------|---------| +| `index.php` | List all theses (main page; hosts the inline CSV import + tabs for list/trash) | +| `add.php` | Add new thesis form | +| `edit.php` | Existing thesis form | +| `recapitulatif.php` | Post-submission recap | +| `cleanup.php` | Orphaned-draft / storage cleanup | +| `system.php` | System dashboard (logs, SMTP/PeerTube status) | +| `contenus.php` | Editable content (pages, contacts) | +| `contenus-edit.php` | Edit a content page | +| `acces.php` | Share-link management | +| `file-access.php` | Restricted-file access requests | +| `account.php` | Admin account / password | +| `login.php` | Login (session) | +| `import.php` | Redirects to `/admin/` (CSV import is inline in `index.php`) | +| `status.php`, `markdown-cheatsheet-fragment.php`, `*fragment.php` | HTMX fragments / helpers | -## File Types +### Backend actions (`app/public/admin/actions/`) -### User-Facing Templates (Root Directory) -Files that display HTML to users: -- **index.php** - Lists all theses with filters and bulk actions -- **add.php** - Form to add a new thesis -- **edit.php** - Form to edit an existing thesis -- **import.php** - CSV import interface -- **recapitulatif.php** - Success confirmation page +Process forms and redirect (no HTML output): -### Backend Scripts (actions/) -Files that process forms and redirect (no HTML output): -- **formulaire.php** - Processes thesis submission from add.php -- **publish.php** - Handles publish/unpublish actions - -### Shared Templates (inc/) -Reusable HTML components: -- **head.php** - HTML head, CSS links, navigation menu -- **footer.php** - HTML footer - -## Workflow - -### Adding a Thesis -1. User visits `add.php` (displays form) -2. User submits form to `actions/formulaire.php` (processes data) -3. On success, redirects to `recapitulatif.php?id=123` -4. On error, redirects back to `add.php` with error message - -### Publishing/Unpublishing -1. User clicks publish/unpublish button in `index.php` -2. Form submits to `actions/publish.php` (processes action) -3. Redirects back to `index.php` with success/error message - -## Security - -- All pages require HTTP Basic Auth (configured in nginx) — primary layer -- All pages require PHP session auth (`AdminAuth::requireLogin()`) — defence-in-depth -- CSRF tokens protect all forms -- File uploads validated and sanitized -- Database queries use prepared statements -- Upload directory outside public/ in production - -See `nginx/PHP_AUTH_LAYER.md` for details on the dual-auth architecture. +- `formulaire.php` — thesis create submission (`ThesisCreateController::submit()`) +- `edit.php` — thesis edit submission (`ThesisEditController::save()`) +- `export-csv.php`, `export-db.php`, `export-files.php` — see `docs/export.md` +- `filepond/` — FilePond async upload endpoints +- many others: `publish`, `delete`, `corbeille` (trash), `draft`, `visibility`, + `tag`, `language`, `form-help*`, `page`, `apropos`, `smtp-test`, + `peertube-*`, `maintenance`, `settings`, `account`, `access-request`, + `acces-etudiante`, `cleanup-*` ## Templates -The `inc/` folder contains shared templates: -- `head.php` - Included at the top of each page (DOCTYPE, CSS, nav) -- `footer.php` - Included at the bottom of each page (closing tags) +View templates live under `app/templates/admin/` (not in `public/`): +- `app/templates/admin/*.php` — page layouts +- `app/templates/admin/partials/` — shared fragments (toasts, dialogs, toc, …) + +The public/partage and form partials live in `app/templates/partials/` and +`app/templates/partage/`. + +## Auth + +- **PHP session auth** (`src/AdminAuth.php`) via `AdminAuth::requireLogin()` is + the only authentication layer. The old nginx `auth_basic` layer has been + removed — see `docs/security.md` and `nginx/docs/PHP_AUTH_LAYER.md`. +- All forms include a CSRF token from `$_SESSION['csrf_token']`. +- Inputs use PDO prepared statements; uploads validated and stored outside the + webroot (`app/storage/`). + +## Bootstrap / routing + +Entry pages bootstrap the app and set up the environment: -Usage: ```php - - - -``` - -## URL Structure - -- `/admin/` - List theses (index.php) -- `/admin/add.php` - Add new thesis -- `/admin/edit.php?id=123` - Edit thesis #123 -- `/admin/import.php` - Import CSV -- `/admin/recapitulatif.php?id=123` - Recap page - -Backend actions (not directly accessed): -- `/admin/actions/formulaire.php` - Form processor -- `/admin/actions/publish.php` - Publish toggle - -## Development - -### Adding a New Page - -1. Create the template in `/admin/yourpage.php`: -```php - - - - - - ``` -2. Add navigation link in `inc/head.php` if needed +`APP_ROOT` is the `app/` directory. Database access is via +`app/src/Database.php`; form logic lives in `app/src/Controllers/` and +`app/src/Form/`. -### Adding a New Action +## URL structure -1. Create the script in `/admin/actions/youraction.php`: -```php - **Note on naming:** XAMXAM was previously *Post-ERG* (and the code was once +> organised under `posterg-website/`, `apps/`, `front-backend/`, +> `/var/www/posterg/`). References to `posterg`, `apps/public/inc/header.php`, +> `posterg.db`, `just serve`, `php-live-reload`, `tests/run-tests.php`, etc. in +> historical docs reflect the old layout. The live paths are now `app/`, +> `app/public/`, `app/storage/xamxam.db`, `just dev`, PHPUnit, and +> `/var/www/xamxam/` on the server. + +## Current reference (kept up to date) + +| Doc | Contents | +|-----|----------| +| [development.md](development.md) | Dev workflow, structure, builds, testing, linting | +| [deployment.md](deployment.md) | Server setup, deploy, backups, rollback | +| [database.md](database.md) | SQLite schema, migrations, tables, common ops | +| [search.md](search.md) | `/search` and `/repertoire` behaviour | +| [export.md](export.md) | CSV / DB / files export + full restore procedure | +| [import.md](import.md) | CSV import format + behaviour | +| [security.md](security.md) | Current security posture | +| [file-uploads.md](file-uploads.md) | Upload surfaces, types, storage layout | +| [CSS.md](CSS.md) | CSS architecture + build | +| [bookmarklet.md](bookmarklet.md) | Form auto-fill test helper | + +## Proposals / plans (proposed, not all implemented) + +| Doc | Status | +|-----|--------| +| [LDAP_AUTH_PLAN.md](LDAP_AUTH_PLAN.md) + [LDAP_SPEC.md](LDAP_SPEC.md) | LDAP login — **not implemented** | +| [monolog-plan.md](monolog-plan.md) | Single Monolog logger replacing AppLogger/AdminLogger/ErrorHandler/Audit — **plan** | +| [de-librairisation.md](de-librairisation.md) | Replace bespoke SMTP/Markdown/HTTP/crypto with libraries (partly done) | +| [refactoring.md](refactoring.md) | Older refactoring proposal | +| [ANALYSIS_STRUCTURE_REORG.md](ANALYSIS_STRUCTURE_REORG.md) | Proposed structure reorg | +| [ANALYSIS_INLINE_JS_CSS_MINIFY.md](ANALYSIS_INLINE_JS_CSS_MINIFY.md) | Inline JS/CSS/minify analysis (largely actioned by the build system) | +| [backup-plan.md](backup-plan.md) | Backup plan — largely implemented (see deployment.md) | +| [repertoire-mobile-propositions.md](repertoire-mobile-propositions.md) | Mobile repertoire UI proposals | +| [cms-migration-plan.html](cms-migration-plan.html) | CMS migration plan | +| [spec-sheet.md](spec-sheet.md) | Original requirements fiche technique | +| `Proposition procédure licences_V2.pdf` | Licence procedure proposal (filename contains non-ASCII/combining chars) | + +## Historical / archived (kept for context — may reference the old codebase) + +| Doc | Contents | +|-----|----------| +| [LIVRAISONS_PAR_MOIS.md](LIVRAISONS_PAR_MOIS.md) | Monthly delivery log grouped by functional family | +| [migration-history.md](migration-history.md) | History of major structural migrations | +| [CURRENT_ISSUES.md](CURRENT_ISSUES.md) | Issue log (2026-05-10) — many since resolved | +| [IMMEDIATE_FIX.md](IMMEDIATE_FIX.md) | One-off fix note | +| [EVIDENCE_SUMMARY.md](EVIDENCE_SUMMARY.md) + [VM_Crash_*.md](VM_Crash_Analysis_FINAL.md) | VM crash investigation (concluded: not the app) | +| [css.md](css.md) | Old Bulma-removal writeup (pre-dates current build system; see CSS.md) | +| [php-vs-flask.md](php-vs-flask.md) | Language choice decision | +| [orm-assessment.md](orm-assessment.md) | ORM evaluation | +| [system-setup.md](system-setup.md) | Old setup notes | +| [SETUP.md](SETUP.md), [SPECS.md](SPECS.md), [TODO.md](TODO.md) | Earlier setup/spec/todo snapshots | +| [SMTP_550_POSTFIX_FIX.md](SMTP_550_POSTFIX_FIX.md) | SMTP troubleshooting record | +| [testing.md](testing.md) | PHP testing best-practices writeup | +| [test-plan.md](test-plan.md) | Manual test plan | +| [autosave-system.md](autosave-system.md), [filepond-crash-analysis.md](filepond-crash-analysis.md), [filepond-race-investigation.md](filepond-race-investigation.md) | Feature/issue deep-dives (status noted in each file) | +| [pi-session-2026-05-10T*.html](pi-session-2026-05-10T18-42-37-234Z_019e1332-ce31-70fa-87a1-aa3495b526a9.html) | Captured session log | +| [ANALYSIS_INLINE_JS_CSS_MINIFY.md](ANALYSIS_INLINE_JS_CSS_MINIFY.md) | *(see proposals)* | +| [bookmarklet.md](bookmarklet.md) | *(kept current — see above)* | + +## Related documentation elsewhere + +- `nginx/docs/` — nginx config, security headers, deployment, HTACCESS→nginx +- `app/storage/README.md` — DB quick-reference (schema link) +- `app/public/admin/README.md` — admin panel structure + +--- + +**Maintenance guidance:** when updating code, update the matching *current +reference* doc in the table above. Leave *historical* docs untouched (they are +read-only context). Move newly-written analysis into the appropriate section. diff --git a/docs/bookmarklet.md b/docs/bookmarklet.md index e57be70..f3f62a5 100644 --- a/docs/bookmarklet.md +++ b/docs/bookmarklet.md @@ -1,111 +1,107 @@ -# Bookmarklet +# Bookmarklet — auto-fill test form -## Auto-fill TFE form (testing) +A drag-to-bookmarks helper that pre-fills an XAMXAM thesis form with dummy data +so a submit can be tested quickly. -Drag the link below to your bookmarks bar, open `/admin/add.php?mode=student`, then click it. Every field gets filled with dummy data so you can hit submit immediately. +> The old `?mode=student` flag and the standalone student form no longer exist. +> Student submissions now use **share links** at `/partage/` (see the +> `share_links` table). The same form template (`app/templates/partials/form/`) +> drives admin add/edit and the partage form, so the field names below mostly +> match. The bookmarklet below targets the **admin** add form +> (`/admin/add.php`). -### Bookmarklet +## Bookmarklet -**[Remplir TFE](javascript:(function()%7Bvar%20f%3Ddocument.querySelector('form.admin-form')%3Bif(!f)return%3Bvar%20set%3Dfunction(n%2Cv)%7Bvar%20e%3Df.querySelector('[name%3D%22'%2Bn%2B'%22]')%3Bif(e)%7Be.value%3Dv%3Be.dispatchEvent(new%20Event('change'%2C%7Bbubbles%3Atrue%7D))%3B%7D%7D%3Bvar%20check%3Dfunction(n)%7Bvar%20e%3Df.querySelector('[name%3D%22'%2Bn%2B'%22]')%3Bif(e)%7Be.checked%3Dtrue%3Be.dispatchEvent(new%20Event('change'%2C%7Bbubbles%3Atrue%7D))%3B%7D%7D%3Bvar%20checkCB%3Dfunction(n%2Cv)%7Bvar%20e%3Df.querySelector('[name%3D%22'%2Bn%2B'[]%22][value%3D%22'%2Bv%2B'%22]')%3Bif(e)%7Be.checked%3Dtrue%3Be.dispatchEvent(new%20Event('change'%2C%7Bbubbles%3Atrue%7D))%3B%7D%7D%3Bset('titre'%2C'TFE%20Test%20%E2%80%94%20Impact%20des%20r%C3%A9seaux%20sociaux%20sur%20la%20pratique%20artistique%20contemporaine')%3Bset('subtitle'%2C'%C3%89tude%20de%20cas%20aupr%C3%A8s%20des%20%C3%A9tudiant%C2%B7es%20de%20l%27ERG')%3Bset('auteurice'%2C'Marie-France%20Dupont')%3Bset('mail'%2C'mfd%40testmail.be')%3Bset('synopsis'%2C'Ce%20travail%20explore%20la%20mani%C3%A8re%20dont%20les%20plateformes%20num%C3%A9riques%20ont%20transform%C3%A9%20les%20processus%20de%20cr%C3%A9ation%20artistique.')%3Bcheck('contact_public')%3Bset('jury_president'%2C'Prof.%20Jean-Luc%20Moreau')%3Bset('jury_promoteur'%2C'Dr.%20Aline%20Vandenberghe')%3Bcheck('jury_promoteur_ext')%3Bset('ann%C3%A9e'%2C'2025')%3Bset('orientation'%2C'4')%3Bset('ap'%2C'3')%3Bset('finality'%2C'1')%3BcheckCB('languages'%2C'1')%3BcheckCB('languages'%2C'2')%3BcheckCB('formats'%2C'1')%3BcheckCB('formats'%2C'3')%3Bset('tag'%2C'r%C3%A9seaux%20sociaux%2Cart%20num%C3%A9rique%2Csociologie%2Cpratique%20artistique')%3Bset('license_id'%2C'7')%3Bset('duration_info'%2C'96%20pages')%3Bset('lien'%2C'https%3A%2F%2Fexample.com%2Ftfe-test')%3Bset('access_type_id'%2C'2')%3Bcheck('cc4r')%3B%7D)())** +Open `/admin/add.php` (logged in), then click the bookmarklet: -### Readable source +``` +javascript:(function(){var set=function(n,v){var e=document.querySelector('[name="'+n+'"]');if(e){e.value=v;e.dispatchEvent(new Event('change',{bubbles:true}));}};var setFirst=function(n,v){var e=document.querySelector('[name="'+n+'"][type="text"]');if(e){e.value=v;e.dispatchEvent(new Event('change',{bubbles:true}));}};var checkCB=function(n,v){var e=document.querySelector('[name="'+n+'[]"][value="'+v+'"]');if(e){e.checked=true;e.dispatchEvent(new Event('change',{bubbles:true}));}};var check=function(n){var e=document.querySelector('[name="'+n+'"]');if(e){e.checked=true;e.dispatchEvent(new Event('change',{bubbles:true}));}};set('titre','TFE Test — Impact des réseaux sociaux');set('subtitle','Étude de cas auprès des étudiant·es de l\'ERG');set('auteurice','Marie-France Dupont');set('contact_visible','public');setFirst('jury_promoteur[]','Dr. Aline Vandenberghe');setFirst('jury_lecteur_interne[]','Prof. Jean-Luc Moreau');setFirst('jury_lecteur_externe[]','Prof. Kim Sølv');setFirst('jury_promoteur_ulb_name[]','Prof. Université');set('synopsis','Ce travail explore l\'impact des plateformes numériques sur la pratique artistique contemporaine.');set('orientation','4');set('ap','3');set('finality','1');checkCB('formats','1');checkCB('formats','3');set('license_id','8');set('duration_pages','96');check('cc2r');set('website_url','https://example.com/tfe-test');set('access_type_id','2');set('contact_interne','mfd@testmail.be');set('jury_points','16.5');})() +``` + +## Readable source ```js (function () { - var f = document.querySelector('form.admin-form'); - if (!f) return; - - var set = function (name, val) { - var e = f.querySelector('[name="' + name + '"]'); - if (e) { e.value = val; e.dispatchEvent(new Event('change', { bubbles: true })); } + var set = function (n, v) { + var e = document.querySelector('[name="' + n + '"]'); + if (e) { e.value = v; e.dispatchEvent(new Event('change', { bubbles: true })); } }; - var check = function (name) { - var e = f.querySelector('[name="' + name + '"]'); + var setFirst = function (n, v) { // first hidden/text field of a jury array + var e = document.querySelector('[name="' + n + '"][type="text"]'); + if (e) { e.value = v; e.dispatchEvent(new Event('change', { bubbles: true })); } + }; + var checkCB = function (n, v) { // checkbox-list (formats[]) value + var e = document.querySelector('[name="' + n + '[]"][value="' + v + '"]'); if (e) { e.checked = true; e.dispatchEvent(new Event('change', { bubbles: true })); } }; - var checkCB = function (name, val) { - var e = f.querySelector('[name="' + name + '[]"][value="' + val + '"]'); + var check = function (n) { + var e = document.querySelector('[name="' + n + '"]'); if (e) { e.checked = true; e.dispatchEvent(new Event('change', { bubbles: true })); } }; // ── Informations du TFE ── - set('titre', 'TFE Test — Impact des réseaux sociaux sur la pratique artistique contemporaine'); + set('titre', 'TFE Test — Impact des réseaux sociaux'); set('subtitle', 'Étude de cas auprès des étudiant·es de l\'ERG'); - set('auteurice', 'Marie-France Dupont'); - set('mail', 'mfd@testmail.be'); - set('synopsis', 'Ce travail explore la manière dont les plateformes numériques ont transformé les processus de création artistique.'); - check('contact_public'); + set('auteurice', 'Marie-France Dupont'); // comma-separated + set('contact_visible', 'public'); // admin mode; partage uses 'mail' - // ── Jury ── - set('jury_president', 'Prof. Jean-Luc Moreau'); - set('jury_promoteur', 'Dr. Aline Vandenberghe'); - check('jury_promoteur_ext'); + // ── Jury (array fields, one row each) ── + setFirst('jury_promoteur[]', 'Dr. Aline Vandenberghe'); + setFirst('jury_lecteur_interne[]', 'Prof. Jean-Luc Moreau'); + setFirst('jury_lecteur_externe[]', 'Prof. Kim Sølv'); + setFirst('jury_promoteur_ulb_name[]', 'Prof. Université'); // ── Cadre académique ── - set('année', '2025'); - set('orientation', '4'); // Installation-Performance - set('ap', '3'); // Atelier Pratiques Situées - set('finality', '1'); // Approfondi - checkCB('languages', '1'); // Français - checkCB('languages', '2'); // Anglais - checkCB('formats', '1'); // Site web - checkCB('formats', '3'); // Vidéo - set('tag', 'réseaux sociaux, art numérique, sociologie, pratique artistique'); + set('synopsis', 'Ce travail explore l\'impact des plateformes numériques sur la pratique artistique contemporaine.'); + set('orientation', '4'); // Installation-Performance + set('ap', '3'); // Atelier Pratiques Situées + set('finality', '1'); // Approfondie + checkCB('formats', '1'); // Site web + checkCB('formats', '3'); // Vidéo - // ── Métadonnées complémentaires ── - set('license_id', '7'); // Tous droits réservés - set('duration_info', '96 pages'); - set('lien', 'https://example.com/tfe-test'); + // ── Métadonnées ── + set('license_id', '8'); // Tous droits réservés + set('duration_pages', '96'); + check('cc2r'); // CC2r licence + set('website_url', 'https://example.com/tfe-test'); set('access_type_id', '2'); // Interne - - // ── Licences (student mode only) ── - check('cc4r'); + set('contact_interne', 'mfd@testmail.be'); + set('jury_points', '16.5'); })(); ``` -### Lookup table reference (schema.sql seed IDs) +## Notes & current field names (verified against the form partials) -| Table | ID | Value | -|---|---|---| -| **orientations** | 1 | Arts Numériques | -| | 2 | Dessin | -| | 3 | Cinéma d'animation | -| | 4 | Installation-Performance | -| | 5 | Peinture | -| | 6 | Photographie | -| | 7 | Sculpture | -| | 8 | Vidéographie | -| | 9 | Graphisme | -| | 10 | Typographie | -| | 11 | Design Numérique | -| | 12 | Illustration | -| | 13 | Bande-Dessinée | -| | 14 | Sérigraphie | -| | 15 | Gravure | -| **ap_programs** | 1 | Narration Spéculative | -| | 2 | Design et Politique du Multiple | -| | 3 | Atelier Pratiques Situées | -| | 4 | LIENS | -| **finality_types** | 1 | Approfondi | -| | 2 | Enseignement | -| | 3 | Spécialisé | -| **languages** | 1 | Français | -| | 2 | Anglais | -| **format_types** | 1 | Site web | -| | 2 | Audio | -| | 3 | Vidéo | -| | 4 | Performance | -| | 5 | Objet éditorial | -| | 6 | Installation | -| | 7 | Autre | -| **license_types** | 1 | CC BY 4.0 | -| | 2 | CC BY-SA 4.0 | -| | 3 | CC BY-ND 4.0 | -| | 4 | CC BY-NC 4.0 | -| | 5 | CC BY-NC-SA 4.0 | -| | 6 | CC BY-NC-ND 4.0 | -| | 7 | Tous droits réservés | -| | 8 | Domaine public | -| **access_types** | 1 | Libre | -| | 2 | Interne | -| | 3 | Interdit | +| Field | Notes | +|-------|-------| +| `titre`, `subtitle`, `auteurice` | `auteurice` is comma-separated | +| `contact_visible` | admin add/edit; the partage form uses `mail` | +| `jury_promoteur[]`, `jury_lecteur_interne[]`, `jury_lecteur_externe[]`, `jury_promoteur_ulb_name[]` | one `input[type=text]` per row (`setFirst` targets the first) | +| `orientation`, `ap`, `finality` | `` of `license_types` | +| `cc2r` | checkbox (renamed from `cc4r`) | +| `want_license` | hidden `0`; set to `1` to show the licence explanation block | +| `duration_pages` | combined duration: pages int; time uses `duration_h` + `duration_m` (values in minutes) | +| `website_url`, `website_label` | the "lien" (site web) fields | +| `access_type_id` | radio: `""`, `1`=Libre, `2`=Interne, `3`=Interdit | +| `contact_interne`, `exemplaire_baiu`, `exemplaire_erg`, `is_published`, `context_note`, `jury_points`, `remarks` | other form fields | + +### Removed / renamed + +- `cc4r` → `cc2r` +- `duration_info` → split into `duration_pages` + `duration_h`/`duration_m` +- `jury_president`, single `jury_promoteur` → jury arrays above (`jury_promoteur[]` etc.) +- `lien` → `website_url`; `mail` in admin mode → `contact_visible` +- `contact_public` checkbox → handled via `authors.show_contact` / `contact_visible` +- `?mode=student` → student flows go through `/partage/` + +### Lookup ids + +`orientations` 4=`Installation-Performance`; `ap_programs` 3=`Atelier Pratiques +Situées`; `finality_types` 1=`Approfondie`; `format_types` 1=`Site web`, +3=`Vidéo`; `license_types` 8=`Tous droits réservés`; `access_types` 2=`Interne`. + +> IDs may shift after DB migrations that re-number reference rows — verify +> against the current `app/storage/schema.sql` seed data if a lookup fails. diff --git a/docs/database.md b/docs/database.md index 7bf2d48..3357c4b 100644 --- a/docs/database.md +++ b/docs/database.md @@ -1,323 +1,253 @@ # Database Reference -Post-ERG SQLite database — schema, configuration, and operations. +XAMXAM SQLite database — schema, configuration, and operations. -**Version:** 1.0 · **Engine:** SQLite 3 · **Mode:** WAL +The database lives at **`app/storage/xamxam.db`**. The canonical schema is +**`app/storage/schema.sql`** (base tables + seed data); incremental changes are +applied through versioned **migrations** in `app/migrations/` (see the +*Schema changes* section). + +> Legacy docs referenced `posterg.db`, `database/schema.sql`, and +> `config/bootstrap.php`. Those are obsolete — the schema now lives under +> `app/storage/` and migrations are handled by `app/migrations/run.php`. --- -## Quick Start +## Engine & mode + +- **Engine:** SQLite 3 +- **Mode:** WAL (`journal_mode=wal`) for safe concurrent reads + hot backups +- **Driver:** PDO (`ext-pdo` + `ext-sqlite3`), wrapped by `app/src/Database.php` + +--- + +## Development quick start ```bash -cd database/ -sqlite3 posterg.db < schema.sql # Create DB -sqlite3 posterg.db "SELECT name FROM sqlite_master WHERE type='table';" -sqlite3 posterg.db "SELECT * FROM orientations;" # Verify seed data +just migrate # run pending migrations (creates DB from schema if missing) +just init-db # create DB from app/storage/schema.sql +just reset-db # rm xamxam.db + init-db +just query # open an interactive sqlite3 shell on app/storage/xamxam.db +sqlite3 app/storage/xamxam.db ".tables" ``` --- -## Configuration +## Migrations -Database paths are centralized in `config/bootstrap.php`: +Migrations live in `app/migrations/`. They are run via +`app/migrations/run.php` (wrapped by `just migrate`) and tracked in the +`_migrations` table. Apply the same way in production with +`just deploy-migrate`. -- **Development**: `APP_ROOT . '/storage/test.db'` (gitignored) -- **Production**: `APP_ROOT . '/storage/posterg.db'` - -The `Database` class (`src/Database.php`) auto-detects: if `test.db` exists → use it, otherwise → use `posterg.db`. Override with `DB_ENV` env var (`test` or `prod`) or pass a custom path to the constructor. - ---- - -## Schema Overview - -### Entity Relationship +Each migration is a numbered file: ``` -authors ──1:N──► thesis_authors ──N:1──► theses -supervisors ──1:N──► thesis_supervisors ──N:1──► theses -keywords ──1:N──► thesis_keywords ──N:1──► theses -languages ──1:N──► thesis_languages ──N:1──► theses -format_types ──1:N──► thesis_formats ──N:1──► theses -orientations ──N:1──► theses -ap_programs ──N:1──► theses -finality_types ──N:1──► theses -access_types ──N:1──► theses -license_types ──N:1──► theses -thesis_files ──N:1──► theses +app/migrations/applied/ +├── 001_add_objet_column.sql +├── … +├── 041_combined_duration.sql +├── 042_fix_contact_columns.sql +└── 043_relabel_promoteur_ulb.php # PHP migrations can run logic too ``` -### Table Categories - -| Category | Tables | -|----------|--------| -| **Core** | `theses`, `authors`, `supervisors`, `thesis_files`, `pages` | -| **Lookup** | `orientations` (15), `ap_programs` (4), `finality_types` (3), `languages` (2+), `format_types` (7), `access_types` (3), `license_types`, `keywords` (dynamic) | -| **Junction** | `thesis_authors`, `thesis_supervisors`, `thesis_keywords`, `thesis_languages`, `thesis_formats` | -| **Views** | `v_theses_full` (admin), `v_theses_public` (published only) | +`schema.sql` is the fully-migrated baseline; it is regenerated from the local +DB so it always reflects the applied set of migrations. --- -## Core Tables +## Tables -### `theses` +31 tables. Grouped by purpose: -| Column | Type | Required | Description | -|--------|------|----------|-------------| -| `id` | INTEGER PK | auto | Primary key | -| `identifier` | TEXT UNIQUE | no | Human-readable ID (e.g., "2025-002") | -| `title` | TEXT | **yes** | Thesis title | -| `subtitle` | TEXT | no | Optional subtitle | -| `year` | INTEGER | **yes** | Academic year | -| `is_doctoral` | BOOLEAN | no | 0=TFE, 1=Doctoral | -| `orientation_id` | INTEGER FK | no | → `orientations` | -| `ap_program_id` | INTEGER FK | no | → `ap_programs` | -| `finality_id` | INTEGER FK | no | → `finality_types` | -| `synopsis` | TEXT | no | ~200 word summary | -| `context_note` | TEXT | no | Jury president note (max 150 words) | -| `remarks` | TEXT | no | Internal remarks | -| `duration_minutes` | INTEGER | no | For audio/video | -| `duration_pages` | INTEGER | no | For written works | -| `file_size_info` | TEXT | no | Free-form size description | -| `access_type_id` | INTEGER FK | no | → `access_types` | -| `license_id` | INTEGER FK | no | → `license_types` | -| `jury_points` | DECIMAL(4,2) | no | Grade (0–20) | -| `jury_note_added` | BOOLEAN | no | Jury context note flag | -| `submitted_at` | DATETIME | no | Student submission | -| `defense_date` | DATETIME | no | Defense date | -| `published_at` | DATETIME | no | Publication date | -| `is_published` | BOOLEAN | no | Publication status | -| `baiu_link` | TEXT | no | Institutional repository link | -| `created_at` | DATETIME | auto | Record creation | -| `updated_at` | DATETIME | auto | Last update (trigger) | +### Core -**Indexes:** `idx_theses_year`, `idx_theses_published`, `idx_theses_identifier`, `idx_theses_orientation`, `idx_theses_ap_program`, `idx_theses_access_type` +| Table | Purpose | +|-------|---------| +| `theses` | The main entity (a TFE or doctoral work) | +| `authors` | People who authored a thesis | +| `supervisors` | Jury members (promoteur / lecteur / président) | +| `thesis_files` | Uploaded files attached to a thesis | +| `theses` subtypes | via `objet` column (`tfe`, `these`, …) and `is_doctoral` | -### `authors` +### Lookup / reference -| Column | Type | Description | -|--------|------|-------------| -| `id` | INTEGER PK | Auto | -| `name` | TEXT NOT NULL | Full name | -| `email` | TEXT | Contact email (optional) | -| `created_at` / `updated_at` | DATETIME | Auto timestamps | +| Table | Notes | +|-------|-------| +| `orientations` (15) | Arts Numériques, Dessin, …, Gravure | +| `ap_programs` (5) | Narration Spéculative, DPM, APS, LIENS, PACS | +| `finality_types` (3) | Approfondie, Enseignement, Spécialisée | +| `languages` | français, anglais, néerlandais, italian (lowercase; expandable) | +| `format_types` | Audio, Vidéo, Site web, Performance, Objet éditorial, Installation, Autre | +| `access_types` (3) | Libre / Interne / Interdit | +| `license_types` | CC BY 4.0 …, Domaine public, Tous droits réservés | +| `tags` | Keywords (dynamic, lowercase-normalised, max 10/thesis) | -**Index:** `idx_authors_email` +### Junction (many-to-many) -### `supervisors` +`thesis_authors`, `thesis_supervisors`, `thesis_languages`, +`thesis_formats`, `thesis_tags`. -| Column | Type | Description | -|--------|------|-------------| -| `id` | INTEGER PK | Auto | -| `name` | TEXT NOT NULL | Full name | -| `created_at` / `updated_at` | DATETIME | Auto timestamps | +### Sharing / access -### `thesis_files` +| Table | Purpose | +|-------|---------| +| `share_links` | Student/partage submission links (slug, password, expiry, archived) | +| `file_access_requests` | Requests to access restricted files | +| `file_access_tokens` | One-time access tokens | +| `file_access_sessions` | Session tokens for granted access | +| `file_access_audit` | Audit trail of access events | -| Column | Type | Description | -|--------|------|-------------| -| `id` | INTEGER PK | Auto | -| `thesis_id` | INTEGER FK | → `theses` (CASCADE) | -| `file_type` | TEXT | `main`, `annex`, `written_part`, `other` | -| `file_path` | TEXT | Relative path | -| `file_name` | TEXT | Original filename | -| `file_size` | INTEGER | Size in bytes | -| `mime_type` | TEXT | MIME type | -| `description` | TEXT | Optional | -| `uploaded_at` | DATETIME | Upload timestamp | +### Integrations / settings / content -### `pages` +| Table | Purpose | +|-------|---------| +| `peertube_settings` | PeerTube video-hosting config (instance, channel, labels) | +| `smtp_settings` | SMTP relay config + notify email (singleton `id=1`) | +| `site_settings` | Feature flags (access types, objets, upload, admin password hash) | +| `pages` | Editable content pages (`about`, `charte`, `licenses`) | +| `apropos_contents` | Structured contact / about content | +| `form_help_blocks` | Help texts on admin/partage forms | +| `system_cache` | Key/value cache | -| Column | Type | Description | -|--------|------|-------------| -| `id` | INTEGER PK | Auto | -| `slug` | TEXT UNIQUE | URL identifier | -| `title` | TEXT NOT NULL | Page title | -| `content` | TEXT | Markdown/HTML | -| `is_published` | BOOLEAN | Default 1 | -| `created_at` / `updated_at` | DATETIME | Auto timestamps | +### Auditing -**Pre-loaded:** `charte`, `about`, `licenses`, `contact` +| Table | Purpose | +|-------|---------| +| `admin_audit_log` | Audit log of admin operations (resource, action, status, IP/UA) | +| `audit_log` | Low-level row audit (actor, action, before/after JSON) | --- -## Lookup Tables +## Key columns — `theses` -### `orientations` (15 predefined) +| Column | Type | Notes | +|--------|------|-------| +| `id` | INTEGER PK | Auto | +| `identifier` | TEXT | Human-readable id (e.g. `2025-003`) | +| `title` | TEXT NOT NULL | | +| `subtitle` | TEXT | | +| `year` | INTEGER NOT NULL | | +| `is_doctoral` | BOOLEAN | `0`=TFE, `1`=doctoral | +| `objet` | TEXT | `'tfe'` default; other objets (`these`, `frart`) gated by site_settings | +| `orientation_id` / `ap_program_id` / `finality_id` | INT FK | Lookup refs | +| `synopsis` / `context_note` / `remarks` | TEXT | | +| `duration_pages`, `duration_minutes` | INT | Combined duration handled at app level | +| `has_annexes` | BOOLEAN | | +| `access_type_id` | INT FK | Libre / Interne / Interdit | +| `license_id` | INT FK | → `license_types` | +| `cc2r` | BOOLEAN | CC2r licence checkbox | +| `license_custom` | TEXT | Free-form licence text | +| `jury_points` | DECIMAL(4,2) | Grade / 20 | +| `jury_note_added` | BOOLEAN | | +| `contact_visible` | TEXT | `'public'`/`'internal'` control for author contact | +| `submitted_at`, `defense_date`, `published_at` | DATETIME | Lifecycle timestamps | +| `is_published` | BOOLEAN | Public visibility flag | +| `status` | TEXT | Default `'active'` | +| `baiu_link` | TEXT | Institutional repo link | +| `exemplaire_baiu`, `exemplaire_erg` | BOOLEAN | Physical copy flags | +| `deleted_at` | DATETIME | Soft delete | +| `created_at` / `updated_at` | DATETIME | Auto (`updated_at` trigger) | -Arts Numériques, Dessin, Cinéma d'animation, Installation-Performance, Peinture, Photographie, Sculpture, Vidéographie, Graphisme, Typographie, Design Numérique, Illustration, Bande-Dessinée, Sérigraphie, Gravure +### `thesis_supervisors` -### `ap_programs` (4) +Tracked via an explicit `role`/flag system rather than separate entity tables: -| Code | Name | -|------|------| -| — | Narration Spéculative | -| DPM | Design et Politique du Multiple | -| APS | Atelier Pratiques Situées | -| LIENS | Lieux, Interdisciplinarités, Écologie, Nécessité, Systèmes | - -### `finality_types` (3) - -Approfondi, Enseignement, Spécialisé - -### `format_types` (7) - -Site web, Audio, Vidéo, Performance, Objet éditorial, Installation, Autre - -### `access_types` (3) - -| Name | Description | -|------|-------------| -| Libre | Full access online + library | -| Interne | Physical only; note online | -| Interdit | No access; note only | - -**Business rule:** Access can only be restricted (Libre → Interne → Interdit), never opened. - -### `languages` - -Français, Anglais (expandable) - -### `keywords` - -Dynamic, grows organically. Max 10 per thesis (application-enforced). - ---- - -## Junction Tables - -All use composite PKs (`thesis_id`, `*_id`) with `ON DELETE CASCADE`. - -| Table | Links | Order column | -|-------|-------|-------------| -| `thesis_authors` | theses ↔ authors | `author_order` | -| `thesis_supervisors` | theses ↔ supervisors | `supervisor_order` | -| `thesis_keywords` | theses ↔ keywords | — | -| `thesis_languages` | theses ↔ languages | — | -| `thesis_formats` | theses ↔ format_types | — | - -**Indexes on junction tables:** `idx_thesis_keywords_thesis`, `idx_thesis_keywords_keyword` (and equivalents for authors) +| Column | Notes | +|--------|-------| +| `role` | `promoteur`, `lecteur`, `president` | +| `is_external` | Lecturer is external | +| `is_ulb` | Promoteur is from the university (UCLouvain) side | --- ## Views -### `v_theses_full` — Admin view - -All theses with joined relationships (GROUP_CONCAT for authors, supervisors, keywords, languages, formats, plus human-readable names for orientation, AP, finality, access type, license). - -### `v_theses_public` — Public view - -Same as `v_theses_full` filtered to `is_published = 1`. Unpublished theses never exposed. +- **`v_theses_full`** — all theses with joined, human-readable fields + (orientations, AP, finality, access, license, authors, supervisors with + role splits, languages, formats, tags/keywords, contact fields). +- **`v_theses_public`** — `v_theses_full` filtered to `is_published = 1`. +- **`v_smtp_active`** — active SMTP settings row. --- -## Automatic Features +## Scaffolded / helper tables -- **Auto-increment IDs:** All PKs use `AUTOINCREMENT` -- **Auto timestamps:** `created_at` defaults to `CURRENT_TIMESTAMP`; `updated_at` refreshed by triggers on UPDATE -- **Cascade deletes:** Deleting a thesis removes all junction + file records +- `_migrations` — applied migration bookkeeping (used by the runner). --- -## Common Operations +## Business rules -### Querying +- **Access:** can only be *restricted* (Libre → Interne → Interdit), never opened back up — enforced in the UI/flow. +- **Keywords (`tags`):** lowercase-normalised, de-duplicated, min 3 (form) / max 10 per thesis. +- **Soft deletes:** reference tables such as `languages` carry `deleted_at`; hard `DELETE` is avoided where recoverability matters. +- **Files:** stored outside the webroot under `app/storage/`; DB rows use storage-relative paths (`theses//_/…`). + +--- + +## Common operations ```sql -- Published theses SELECT * FROM v_theses_public ORDER BY year DESC; --- Single thesis (admin) +-- Single thesis (admin view) SELECT * FROM v_theses_full WHERE id = ?; -- By year + orientation SELECT * FROM v_theses_public WHERE year = 2025 AND orientation = 'Arts Numériques'; --- By keyword +-- Search by keyword (tag) SELECT DISTINCT t.* FROM theses t -JOIN thesis_keywords tk ON t.id = tk.thesis_id -JOIN keywords k ON tk.keyword_id = k.id -WHERE k.keyword = 'écologie' AND t.is_published = 1; +JOIN thesis_tags tt ON t.id = tt.thesis_id +JOIN tags g ON tt.tag_id = g.id +WHERE g.name = 'écologie' AND t.is_published = 1; -- Theses per year SELECT year, COUNT(*) FROM theses WHERE is_published = 1 GROUP BY year ORDER BY year DESC; --- Unpublished (admin) -SELECT identifier, title, submitted_at FROM theses -WHERE submitted_at IS NOT NULL AND is_published = 0 ORDER BY submitted_at DESC; -``` - -### Inserting - -```sql -INSERT INTO authors (name, email) VALUES ('Marie Dupont', 'marie@example.com'); -INSERT INTO theses (identifier, title, year, orientation_id, finality_id, synopsis) - VALUES ('2026-001', 'Mon Titre', 2026, 8, 1, 'Synopsis...'); -INSERT INTO thesis_authors (thesis_id, author_id, author_order) VALUES (1, 5, 1); -INSERT OR IGNORE INTO keywords (keyword) VALUES ('performance'); -INSERT INTO thesis_keywords (thesis_id, keyword_id) - SELECT 1, id FROM keywords WHERE keyword = 'performance'; -``` - -### Updating - -```sql +-- Publish a thesis UPDATE theses SET is_published = 1, published_at = CURRENT_TIMESTAMP WHERE id = 5; -UPDATE theses SET jury_points = 16.5, context_note = '…', jury_note_added = 1 WHERE id = 5; +``` + +> Note: keyword/contact fields in `v_theses_*` are `GROUP_CONCAT` aggregates — +> prefer joining the junction tables (`thesis_tags`/`tags`) when you need +> per-row access. + +--- + +## Backup & maintenance + +```bash +just backup # SQL dump → app/storage/backup_.sql +just backup-snapshot # WAL-safe hot backup + gzip → app/storage/backups/ +``` + +On the server, backups are scheduled to `/var/backups/xamxam/` via +`/etc/cron.d/xamxam-backup` (see [deployment.md](deployment.md)). + +Manual maintenance: + +```bash +sqlite3 app/storage/xamxam.db "VACUUM;" # after large deletes +sqlite3 app/storage/xamxam.db "ANALYZE;" +sqlite3 app/storage/xamxam.db "PRAGMA integrity_check;" # expect "ok" ``` --- -## Backup & Maintenance +## Schema changes -### Backup +1. Back up first: `cp app/storage/xamxam.db app/storage/xamxam.db.bak` +2. Add a numbered migration file under `app/migrations/applied/` + (`.sql` for DDL/DML, `.php` when data logic is needed). +3. Run `just migrate` (or `just deploy-migrate` on the server) — idempotent, + tracked via `_migrations`. +4. When appropriate, regenerate `app/storage/schema.sql` from the applied set + so it stays a faithful baseline. -```bash -# File copy (simplest) -cp posterg.db backups/posterg_$(date +%Y%m%d).db - -# SQL dump (portable) -sqlite3 posterg.db .dump > backups/posterg_$(date +%Y%m%d).sql -``` - -### Maintenance - -```bash -sqlite3 posterg.db "VACUUM;" # Reclaim space (after large deletes, monthly) -sqlite3 posterg.db "ANALYZE;" # Update query stats (after schema/data changes) -sqlite3 posterg.db "PRAGMA integrity_check;" # Verify → should output "ok" -sqlite3 posterg.db "PRAGMA journal_mode=WAL;" # Enable WAL for better concurrency -``` - -### Recovery - -```bash -sqlite3 posterg.db ".recover" | sqlite3 recovered.db # Corrupted DB -sqlite3 posterg.db .dump | sqlite3 new.db # Dump + reimport -``` - ---- - -## Performance Notes - -- All critical foreign keys and search fields are indexed -- Views pre-compute joins for common queries -- For 1000+ theses: ensure WAL mode, run `ANALYZE` periodically, consider `VACUUM` -- Cache size: `PRAGMA cache_size=-64000;` (64MB) -- Memory-mapped I/O: `PRAGMA mmap_size=268435456;` (256MB) - ---- - -## Schema Changes - -### Making changes - -1. Always backup first: `cp posterg.db posterg_before.db` -2. Test on backup: `sqlite3 posterg_test.db < migration.sql` -3. Use transactions: wrap ALTER/INSERT in `BEGIN; … COMMIT;` -4. Document in `storage/migrations/` with numbered SQL files - -### Change request format +Change request format: ``` Table: [table_name] diff --git a/docs/deployment.md b/docs/deployment.md index d1f25f9..55ffaca 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -1,204 +1,201 @@ # Deployment -Server setup, deployment, and rollback procedures for Post-ERG. +Server setup, deployment, backups, and rollback procedures for XAMXAM. --- -## One-Time Server Setup +## Overview -Run before first deploy: +- Production host: `xamxam` (over SSH), app root `/var/www/xamxam/` +- Files are pushed with `rsync` — **there is no git repo on the remote** +- Web / FPM user: `www-data`, app group: `xamxam` +- The **DocumentRoot** is `app/public/`, but on the server the code lives flat + under `/var/www/xamxam/` (Composer autoload path adjusted to `src/`) +- SQLite database at `/var/www/xamxam/storage/xamxam.db` + +Deployment is orchestrated through the `justfile` (`deploy` group). + +--- + +## One-time server setup ```bash -just setup-server +ssh xamxam +sudo mkdir -p /var/www/xamxam +sudo chown www-data:xamxam /var/www/xamxam +sudo chmod 775 /var/www/xamxam +exit ``` -This creates `/var/www/posterg/` with correct ownership/permissions: -- Owner: `www-data:posterg` -- Directories: **2775** (setgid — new files inherit `posterg` group) -- Files: **664** -- Database files: **660** +Then from local, deploy once and apply the nginx config + verify permissions: -> **Important:** After running `setup-server`, log out and back in on the server (or `newgrp posterg`) so group membership is active before deploying. +```bash +just deploy +just deploy-nginx +``` -### Why setgid (2775)? +For a full initial rollout including backup + cleanup cron jobs: -rsync uses `--chown=www-data:posterg`. Both `padlock` and `www-data` must write to dirs. With `2775 + group=posterg`, new subdirs inherit the group automatically. +```bash +just deploy-all-first # deploy + deploy-backup + deploy-cleanup-cron +``` --- ## Deploying +| Command | Purpose | +|---------|---------| +| `just deploy` | Full deploy: build + code + Composer deps + migrations + env + permissions check | +| `just deploy-code` | rsync app files + nginx config + permissions (no Composer, no migrations) | +| `just deploy-deps` | Sync composer.{json,lock} → server, then `composer install`/`dump-autoload` | +| `just deploy-migrate` | Run pending DB migrations on the server | +| `just deploy-env` | Upload `app/.env` (only if the remote `.env` is absent — never overwrites a key) | +| `just deploy-nginx` | Upload + apply + reload nginx config | +| `just deploy-db` | Push local `xamxam.db` → remote (**refuses** if a remote DB already exists) | +| `just deploy-verify-permissions` | Check ownership / permissions on the server | + +> ℹ️ **First deploy?** After `just deploy`, run `just deploy-backup` to install +> the backup script + cron jobs. + +### Environment file & re-encryption + +`app/.env` holds secrets (e.g. `APP_KEY`). `deploy-env` will **not** overwrite a +remote `.env` that already has an `APP_KEY`. If you rotate `APP_KEY`, re-encrypt +the SMTP password and push the new key: + ```bash -just deploy # Push all app files -just deploy-db # Push initial database (aborts if remote DB exists) -just deploy-nginx # Push + apply nginx config +just reencrypt-password # runs scripts/reencrypt-smtp-password.php on server +just deploy-env ``` -### First-Time Deployment +--- -Since we moved from `/var/www/html/` to `/var/www/posterg/`: +## Backups -1. **Setup server directory** (one time): - ```bash - just setup-server - ``` +SQLite backups are taken with a WAL-safe hot backup (`sqlite3 .backup`) then +gzipped to `/var/backups/xamxam/` on the server. -2. **Deploy application**: - ```bash - just deploy - ``` - Uploads to `/var/www/posterg/`, excludes tests/docs/vendor. +| Command | Purpose | +|---------|---------| +| `just deploy-backup` | Install backup script + cron jobs (one-shot) | +| `just deploy-backup-script` | Install `/usr/local/bin/backup-sqlite.sh` | +| `just deploy-backup-cron` | Install `/etc/cron.d/xamxam-backup` (hourly 30d + daily 90d) + dirs/log | +| `just deploy-check-backup-log` | Tail `/var/log/sqlite-backup.log` | +| `just deploy-list-backups` | List backups on the server | +| `just trigger-backup` | Run the backup script now | +| `just test-restore ` | Fetch + decompress + verify a remote snapshot | -3. **Deploy nginx config**: - ```bash - just deploy-nginx - ssh posterg - sudo bash /tmp/deploy-production.sh - sudo systemctl reload nginx - ``` +Retention: hourly backups kept 30 days, nightly (02:00) backups kept 90 days. +Backup files: `/var/backups/xamxam/db-.db.gz`. -4. **Verify**: - ```bash - just server-status - curl -I https://posterg.erg.be/ # 200 ✓ - curl -I https://posterg.erg.be/admin/ # 200 ✓ - curl -I https://posterg.erg.be/storage/ # 404 ✓ - ``` +Draft cleanup is handled by a separate cron (`/etc/cron.d/xamxam-cleanup`), +installed via `just deploy-cleanup-cron`, logging to +`/var/log/xamxam-cleanup.log`. Verify with `just deploy-check-cleanup-log`. -### Subsequent Deployments +--- + +## Permissions model + +Managed/simulated by the justfile and verified by `deploy-verify-permissions`: + +- Ownership: `www-data:xamxam` +- Directories: **2775** (setgid — new files inherit the `xamxam` group) +- Regular files: **664** +- `storage/xamxam.db` and other `*.db`: **660** +- `app/.env`: **640** + +The nginx/`deploy-server.sh` step (`just deploy-nginx`, or +`sudo DEPLOY_USER=$USER bash /tmp/deploy-server.sh` via `just deploy-script`) +fixes permissions and installs the nginx config. + +--- + +## Storage migration + +If paths moved (e.g. legacy upload locations), a one-off migration script can +rewrite stored paths on the server: ```bash +just deploy-migrate-storage # apply +just deploy-migrate-storage --dry-run # dry-run only +``` + +--- + +## Rollback + +Because there is no repo on the remote, rollback means restoring files and/or +restoring the database from a backup. + +### Restore the database + +```bash +ssh xamxam +sudo systemctl stop nginx +cp /var/backups/xamxam/db-.db.gz /tmp/restore.db.gz +gunzip -c /tmp/restore.db.gz > /var/www/xamxam/storage/xamxam.db +chown www-data:xamxam /var/www/xamxam/storage/xamxam.db +chmod 660 /var/www/xamxam/storage/xamxam.db +sudo systemctl start nginx +``` + +### Restore application files + +Re-run `just deploy` from a good local state. In jj you can jump back to a +previous commit and redeploy: + +```bash +jj log # find a known-good change +jj edit # work from that revision just deploy ``` --- -## Server Directory Structure - -``` -/var/www/posterg/ # Application root (private) -├── public/ # DocumentRoot (nginx points here) -│ ├── index.php -│ ├── search.php -│ ├── memoire.php -│ ├── admin/ -│ └── assets/ -├── includes/ # Templates (private) -├── config/ # Configuration (private) -├── storage/ # Database + uploads (private) -│ ├── posterg.db -│ └── theses/ -├── src/ # PHP classes (private) -└── scripts/ # Admin tools (private) -``` - -**Nginx DocumentRoot:** `/var/www/posterg/public/` - -Only `public/` is web-accessible. Everything else is physically private. - ---- - -## Managing Admin Users +## Verify after deploy ```bash -ssh posterg "sudo bash /var/www/posterg/scripts/manage-admin-users.sh" -``` - -Interactive menu for adding/changing/deleting htpasswd entries at `/etc/nginx/.htpasswd-posterg`. - ---- - -## Security Verification - -After every deploy, verify private files are inaccessible: - -```bash -curl -I https://posterg.erg.be/storage/test.db # Must 404 -curl -I https://posterg.erg.be/config/bootstrap.php # Must 404 -curl -I https://posterg.erg.be/src/Database.php # Must 404 +curl -I https://xamxam.erg.be/ # expect 200 +curl -I https://xamxam.erg.be/admin/ # expect 200 +curl -I https://xamxam.erg.be/storage/ # expect 404 (blocked) +just deploy-verify-permissions # expect "All permissions OK" ``` --- ## Troubleshooting -### rsync Permission Denied - -```bash -just setup-server # Fixes directory permissions -# Then log out/in on server and retry -``` - -Manual fix: -```bash -ssh posterg -sudo chown -R www-data:posterg /var/www/posterg -sudo find /var/www/posterg -type d -exec chmod 2775 {} \; -sudo find /var/www/posterg -type f -exec chmod 664 {} \; -sudo chmod 660 /var/www/posterg/storage/*.db -``` - -### Nginx 403 Forbidden - -```bash -ssh posterg -sudo find /var/www/posterg -type d -exec chmod 2775 {} \; -sudo find /var/www/posterg -type f -exec chmod 664 {} \; -sudo chmod 660 /var/www/posterg/storage/*.db -``` - -### Database Permission Error - -```bash -ssh posterg -sudo chown www-data:posterg /var/www/posterg/storage/posterg.db -sudo chmod 660 /var/www/posterg/storage/posterg.db -``` - -### Nginx 500 / Site 404 - -Check nginx DocumentRoot: -```bash -ssh posterg "grep 'root ' /etc/nginx/sites-available/posterg" -# Should show: root /var/www/posterg/public; -``` - -### Admin 404 - -Nginx may still use old `/formulaire/` location. Update `nginx/posterg.conf` to use `/admin/`. +- **502 Bad Gateway** + ```bash + sudo systemctl status php8.4-fpm + sudo systemctl restart php8.4-fpm + ``` +- **Nginx config error** + ```bash + sudo nginx -t + ``` +- **`deploy-db` refuses to run** + The remote DB already exists. Remove it manually only if you intend to + overwrite production data. +- **Composer did not pick up new classes** + `deploy-deps` runs `composer dump-autoload` when the lock checksum is + unchanged; if that still fails, force a reinstall. +- **Backup/schedule not running** + Confirm the cron files are installed (`/etc/cron.d/xamxam-backup`, + `/etc/cron.d/xamxam-cleanup`) and the log files are writable by `www-data`. --- -## Rollback - -If something goes wrong: - -```bash -# Restore old nginx config -ssh posterg -sudo cp /etc/nginx/sites-available/posterg.backup /etc/nginx/sites-available/posterg -sudo systemctl reload nginx - -# Or restore old site (if backed up) -sudo rm -rf /var/www/posterg -sudo mv /var/www/html.backup /var/www/html -``` - -With jj: -```bash -jj log -jj edit -``` - ---- - -## Commands Reference +## Selected commands reference | Command | Purpose | |---------|---------| -| `just setup-server` | Create `/var/www/posterg/` (first time only) | -| `just deploy` | Deploy application files | -| `just deploy-nginx` | Update nginx configuration | -| `just deploy-db` | Deploy database file | -| `just server-status` | Check server health | -| `just server-logs` | View server logs | +| `just deploy` | Full deploy (build + code + deps + migrate + env + perms) | +| `just deploy-nginx` | Apply nginx config | +| `just deploy-backup` | Install backup script + cron | +| `just deploy-cleanup-cron` | Install orphaned-draft cleanup cron | +| `just deploy-list-backups` | List server backups | +| `just trigger-backup` | Run backup now | +| `just reencrypt-password ` | Re-encrypt SMTP password after key rotation | +| `just test-restore ` | Verify a snapshot | diff --git a/docs/development.md b/docs/development.md index 9b94fa2..33f9fac 100644 --- a/docs/development.md +++ b/docs/development.md @@ -1,272 +1,158 @@ # Development Guide -Setup, workflow, testing, and live reload for Post-ERG development. +Setup, workflow, building assets, and testing for XAMXAM development. --- -## Quick Start +## Requirements + +- **PHP** ≥ 8.4 with `ext-json`, `ext-openssl`, `ext-pdo` (and `ext-sqlite3` for the local DB) +- **Composer** for PHP dependencies +- **Node.js / npm** for frontend asset builds (rolldown, lightningcss, biome) +- **SQLite3** CLI (for `just query`, `just init-db`) + +## One-time setup + +From the repo root, install dependencies (manually — there is no `just` recipe +for these): ```bash -just setup # One time: clone php-live-reload + setup directories -just serve # Start dev server at http://localhost:8000 +composer install # PHP deps (vendor/) +npm ci # JS build deps (node_modules/) +just migrate # create/update the SQLite DB from schema + migrations ``` -One unified server serves both: -- **Public site:** http://localhost:8000 -- **Admin panel:** http://localhost:8000/admin/ - -Live reload is enabled automatically — browser refreshes when you save files. - ---- +`just setup` exists but its backing script (`scripts/setup-dev.sh`) is **stale** +— it still clones the old `php-live-reload` library and creates legacy +`admin/data/` directories. Live-reload now ships inside the app +(`app/public/live-reload.php`), and assets are built with rolldown/lightningcss, +not the live-reload watcher. Prefer the explicit `composer install` + `npm ci` +above. ## Project Structure ``` -posterg-website/ -├── public/ # DocumentRoot (web-accessible) -│ ├── index.php # Homepage -│ ├── search.php # Search/répertoire -│ ├── memoire.php # Thesis detail -│ ├── admin/ # Admin panel -│ └── assets/ # CSS, fonts, images -├── includes/ # Template partials (header, footer) -├── config/ # Configuration (bootstrap.php) -├── src/ # PHP classes (Database, AdminAuth, RateLimit) -├── storage/ # Database + uploads (private) -├── database/ # Schema + migrations -├── tests/ # Test suite -├── nginx/ # Server configuration -├── scripts/ # Deployment/admin scripts -└── vendor/ # Third-party (gitignored, dev only) +xamxam/ +├── app/ # All application code (the project root on the server) +│ ├── bootstrap.php # App bootstrapping (constants, autoload, config) +│ ├── router.php # Router for the PHP built-in dev server +│ ├── public/ # DocumentRoot (web-accessible only) +│ │ ├── index.php # Front controller / entry point +│ │ ├── request-access.php +│ │ ├── live-reload.php +│ │ ├── admin/ # Admin panel +│ │ ├── partage/ # Student submission via share links +│ │ └── assets/ # Built CSS/JS, fonts, images (dist/ is generated) +│ ├── src/ # PHP classes +│ │ ├── Controllers/ # Request controllers (Home, Search, Tfe, Export, …) +│ │ ├── Form/ # Form helper +│ │ ├── Database.php # DB access + queries +│ │ ├── AdminAuth.php # Session auth for admin +│ │ ├── RateLimit.php +│ │ ├── FilepondHandler.php +│ │ ├── PeerTubeService.php +│ │ └── … # (see list in src/) +│ ├── templates/ # Template partials (public, admin, partials, partage) +│ ├── storage/ # Database + uploads (private) +│ │ ├── xamxam.db # SQLite database +│ │ ├── schema.sql # Base schema +│ │ ├── theses/ tfe/ # Uploaded files +│ │ └── cache/ logs/ # Runtime data +│ └── migrations/ # Migration runner + applied migrations +├── scripts/ # Build / deploy / utility scripts +├── tests/phpunit/ # PHPUnit tests +├── nginx/ # Server configuration + docs +├── deploy/ # Cron configs (backup, cleanup) +├── justfile # Task runner +├── composer.json / package.json +└── TODO.md ``` ---- - ## Development Workflow -### Starting Development +### Start the dev server ```bash -just serve +just dev ``` -### Making Changes +This runs a one-shot asset build, applies pending migrations, opens +`http://127.0.0.1:8000/` (public) and `/admin/` (admin), then starts the PHP +built-in server (`php -S 127.0.0.1:8000`) with a `chokidar` watcher that +rebuilds CSS/JS on change. -1. Edit PHP/CSS files — browser auto-refreshes -2. Run tests: `just test` -3. Check syntax: `just syntax` -4. Deploy: `just deploy` +- **Public site:** `http://127.0.0.1:8000/` +- **Admin panel:** `http://127.0.0.1:8000/admin/` -### Database Operations +Stop it with `just stop`. + +If you only need the server without browsers/ui, run `just dev-build` first, +then start the PHP built-in server manually with the `php -d … -S` command +shown in the `dev` recipe (or add a `just serve` alias locally). + +### Frontend assets + +Source CSS lives in `app/public/assets/css/`, source JS in +`app/public/assets/js/app/`. They are **bundled and minified** into +`app/public/assets/dist/` by `scripts/build*.mjs` (rolldown + lightningcss). ```bash -just stats # View database stats -just query # Open SQLite shell -just show 42 # Show thesis by ID -just reset-db # Reset database -just fixtures # Create sample data -just backup # Backup database +just dev-build # one-shot build (quicker output than full) +just build # full build +just build-css # CSS only +just build-js # JS only +just build-check # verify build output is up to date ``` ---- - -## Live Reload - -### What It Does - -Automatically refreshes your browser when you save PHP/CSS/JS files. No browser extension needed. - -### Setup (One Time) - -```bash -just setup -``` - -Clones `php-live-reload` into `vendor/` (gitignored). - -### How It Works - -- Conditionally included in `header.php` when `php_sapi_name() === 'cli-server'` -- JavaScript polls server for file changes → browser refreshes -- **Never active in production** (different SAPI, vendor/ not deployed) - -### Detection - -```php - - - -``` - -### Troubleshooting - -```bash -ls -la vendor/php-live-reload/ # Check installed -just setup # Reinstall if missing -curl -s http://localhost:8000/ | grep live-reload # Verify script included -``` - ---- +There is no live-reloading in production — `app/public/live-reload.php` only +activates under the PHP built-in server. ## Testing -### Test Structure - -``` -tests/ -├── run-tests.php # Main test runner -├── Unit/ # Unit tests -│ ├── DatabaseTest.php -│ └── RateLimitTest.php -├── Integration/ # Integration tests -│ └── SearchTest.php -└── Security/ # Security tests - └── SecurityTest.php -``` - -### Running Tests +PHPUnit is used. Configuration lives in `phpunit.xml`, tests in `tests/phpunit/` +(with `tests/bootstrap.php` and `tests/TestDatabase.php` helpers). ```bash -just test # Run all tests -just test-unit # Unit tests only -just test-integration # Integration tests only -just test-security # Security tests only -just syntax # Check PHP syntax +just test # run all PHPUnit tests +just test-coverage # run with HTML coverage into coverage/ ``` -### Writing Tests - -1. Choose type: Unit / Integration / Security -2. Create test file in appropriate `tests/` subdirectory -3. Follow template: - -```php -getMessage() . "\n"; - return false; -} -``` - -4. Add to `tests/run-tests.php` `$testFiles` array -5. Run: `just test` - ---- - -## Common Tasks - -### Create a New Page - -```php - -
-
-

New Page

-
-
- -``` - -### Add a Database Method - -1. Edit `src/Database.php` -2. Add method to the class -3. Write test in `tests/Unit/` -4. Run: `just test-unit` - -### Update CSS - -1. Edit `public/assets/posterg.css` -2. Browser auto-refreshes -3. Increment cache-bust in header: `posterg.css?v=N` - ---- - -## Justfile Commands - -### Development - -| Command | Description | -|---------|-------------| -| `just setup` | Setup dev environment (one-time) | -| `just serve` | Start dev server with live reload | -| `just stop` | Stop dev server | -| `just logs` | View dev logs | - -### Testing - -| Command | Description | -|---------|-------------| -| `just test` | Run all tests | -| `just test-unit` | Unit tests only | -| `just test-integration` | Integration tests only | -| `just test-security` | Security tests only | -| `just syntax` | Check PHP syntax | - -### Database - -| Command | Description | -|---------|-------------| -| `just stats` | Database statistics | -| `just query` | Open SQLite shell | -| `just show ` | Show thesis by ID | -| `just reset-db` | Reset test database | -| `just fixtures` | Create sample data | -| `just backup` | Backup database | - -### Deployment - -| Command | Description | -|---------|-------------| -| `just deploy` | Deploy complete site | -| `just deploy-nginx` | Deploy nginx config | -| `just deploy-db` | Deploy database | -| `just server-status` | Check server health | -| `just server-logs` | View server logs | - ---- - -## Debugging +### Linting / formatting ```bash -just logs # View error logs -tail -f error.log # Direct log monitoring - -# PHP errors in browser (temporary): -# Add to PHP file: -ini_set('display_errors', 1); -error_reporting(E_ALL); - -# Database issues: -just stats # Check DB exists and has data -just query # Open SQLite shell +just lint-php # phpstan (static analysis) + php-cs-fixer (coding standards) +just lint-css # biome lint on app/public/assets/css/ +just lint-js # biome lint on app/public/assets/js/app/ + scripts/ +just lint # all linters +just fix # auto-fix (biome + php-cs-fixer) ``` -### Server Won't Start +## Database Operations ```bash -just stop # Kill existing process -just setup # Reinstall php-live-reload +just migrate # run pending migrations +just init-db # create/reset DB from app/storage/schema.sql +just reset-db # rm DB + init-db +just query # open an interactive SQLite shell +just backup # SQL dump into app/storage/backup_.sql +just backup-snapshot # WAL-safe hot backup + gzip into storage/backups/ +just cleanup-drafts [--no-dry-run] # remove orphaned drafts > 24h ``` -### Database Errors +See [database.md](database.md) for the full schema reference. + +## Deployment + +See [deployment.md](deployment.md). Files are pushed to the server with +`just deploy` (rsync → `xamxam:/var/www/xamxam/`). There is no git repo on +the remote. + +## Troubleshooting ```bash -just reset-db # Reset from schema -just fixtures # Repopulate with sample data -just test # Verify everything works +just logs # tail the dev error log (error.log) +just stop # kill the dev server / asset watcher ``` + +If the browser doesn't hot-reload, confirm the asset watcher is still running +and the rebuild succeeded (`just dev-build`). diff --git a/docs/export.md b/docs/export.md index 12de40e..8f62c1d 100644 --- a/docs/export.md +++ b/docs/export.md @@ -106,7 +106,7 @@ sudo systemctl stop nginx # Remplacer le fichier SQLite cp xamxam-db-AAAA-MM-JJ.sqlite /var/www/xamxam/storage/xamxam.db chown www-data:www-data /var/www/xamxam/storage/xamxam.db -chmod 640 /var/www/xamxam/storage/xamxam.db +chmod 660 /var/www/xamxam/storage/xamxam.db ``` ### 2. Restaurer les fichiers @@ -183,9 +183,11 @@ d'exécution raisonnable (`max_execution_time ≥ 60s`) est recommandé. ### Colonnes du CSV d'export -Le CSV utilise la même structure que l'import (21 colonnes). Il est -directement ré-importable sans modification. Voir [`docs/import.md`](import.md) -pour le détail des colonnes. +Le CSV utilise les mêmes colonnes que l'import (les en-têtes proviennent de +`ExportController::CSV_HEADERS`, 26 colonnes dont `CC2r`, `Exemplaire BAIU` et +`Exemplaire ERG`). Il est directement ré-importable — l'import ignore les +colonnes purement d'export. Voir [`docs/import.md`](import.md) pour le détail +des colonnes et de la logique d'import. --- diff --git a/docs/file-uploads.md b/docs/file-uploads.md index 8492576..946d7bb 100644 --- a/docs/file-uploads.md +++ b/docs/file-uploads.md @@ -93,35 +93,34 @@ Files whose MIME type is `application/octet-stream` are accepted **only if their Max size: **20 MB**. -### Banner image (`banner` input) +### Banner image -| Extension | MIME type | -|-----------|-----------| -| `.jpg` / `.jpeg` | `image/jpeg` | -| `.png` | `image/png` | -| `.webp` | `image/webp` | - -Landscape format recommended (4:1 ratio). Max size: **20 MB**. +> Removed — the home-page banner was merged into covers (migration +> `028_drop_banner_path.sql`). There is no separate banner upload anymore. --- ## Size limits -| Limit | Value | -|-------|-------| -| Per-file limit (TFE content files) | **500 MB** | -| PHP `upload_max_filesize` | 512 MB | -| PHP `post_max_size` | 520 MB | -| Cover image | 20 MB | -| Banner image | 20 MB | +Per-field limits are enforced in `app/src/Controllers/validate-file-fragment-shared.php` +(server-side `finfo` + size check on every upload): -The PHP limits are set in: +| Field | Max size | Accepted content | +|-------|----------|------------------| +| `tfe` (main files) | **500 MB** default; **PDF capped at 100 MB**; **video/audio up to 5 GB** | PDF, images, video, audio, archives | +| `annexes` | **500 MB** (PDF capped at 100 MB, video/audio up to 5 GB) | PDF, archives, images, media | +| `couverture` (cover) | **20 MB** | JPG / PNG / WEBP | +| `note_intention` | **100 MB** | PDF | -| File | Applies to | -|------|------------| -| `app/public/.htaccess` | Apache (`mod_php`) | -| `app/public/.user.ini` | PHP-FPM / nginx | -| `justfile` — `serve` recipe | PHP built-in dev server (`php -S` ignores both files above, so limits are passed via `-d` flags) | +(Admins bypass validation entirely — `admin_mode=1`.) + +The PHP engine limits are set as follows: + +| File | Applies to | `upload_max_filesize` | `post_max_size` | +|------|------------|----------------------|------------------| +| `app/public/.user.ini` | PHP-FPM / nginx | 8192M | 8704M | +| `app/public/.htaccess` | Apache (`mod_php`) | 512M | 1024M | +| `justfile` — `dev` recipe | PHP built-in dev server (`php -S` ignores the files above, so limits are passed via `-d` flags) | 8192M | 8704M | For environments that require different limits, edit all three. @@ -160,8 +159,6 @@ Files are stored outside the webroot in `app/storage/`. app/storage/ ├── covers/ │ └── .jpg # cover images -├── banners/ -│ └── .jpg # home-page banners └── theses/ └── / └── _/ @@ -203,8 +200,9 @@ CREATE TABLE thesis_files ( file_size INTEGER, -- bytes mime_type TEXT, description TEXT, -- legacy caption field - display_label TEXT, -- per-file caption (migration 007) - sort_order INTEGER NOT NULL DEFAULT 0, -- display order (migration 007) + display_label TEXT, -- per-file caption + sort_order INTEGER NOT NULL DEFAULT 0, -- display order + file_hash TEXT, -- stored hash (optional) uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (thesis_id) REFERENCES theses(id) ON DELETE CASCADE ); @@ -225,7 +223,10 @@ Files are queried ordered by `sort_order ASC, uploaded_at ASC`. | `app/templates/partials/form/fieldset-files.php` | Upload UI partial (add / partage forms) | | `app/templates/admin/edit.php` | Edit-form files section (sortable existing files + new upload queue) | | `app/templates/public/tfe.php` | Public rendering of all file types | -| `app/public/assets/js/file-upload-queue.js` | SortableJS-backed upload queue + legacy preview | +| `app/src/Controllers/FilepondHandler.php` + `app/public/assets/js/app/file-upload-filepond.js` | FilePond-backed upload queue | | `app/public/.htaccess` | PHP upload limits (Apache) | | `app/public/.user.ini` | PHP upload limits (PHP-FPM / nginx) | | `app/migrations/applied/007_thesis_files_sort_and_label.sql` | DB migration adding `sort_order` + `display_label` | + +> Note: the upload UI was migrated from the legacy SortableJS `file-upload-queue.js` +> to **FilePond** (`file-upload-filepond.js` + `FilepondHandler.php`). diff --git a/docs/import.md b/docs/import.md index 0557d95..3803755 100644 --- a/docs/import.md +++ b/docs/import.md @@ -1,153 +1,108 @@ -# CSV Import Format Specification +# CSV Import -## File Format +Import of theses from the admin panel, plus the CSV structure and behaviour. -- **Encoding**: UTF-8 -- **Delimiter**: Comma (`,`) -- **Header Rows**: First 4 rows are skipped during import - - Row 1: Empty - - Row 2: Headers (French labels) - - Row 3: Description row - - Row 4: Column names -- **Data Rows**: Start from row 5 onwards +> **Where it lives:** the CSV import is handled **inline** in +> `app/public/admin/index.php` — there is no separate `import.php` action. The +> `/admin/import.php` file only redirects to `/admin/`. -## Column Structure +--- -The CSV must contain exactly 21 columns in this order: +## File format -| Index | Field Name | Required | Type | Description | -|-------|------------|----------|------|-------------| -| 0 | identifier | No | String | Unique identifier for the thesis | -| 1 | title | **Yes** | String | Thesis title | -| 2 | subtitle | No | String | Thesis subtitle | -| 3 | authors | No | String | Author(s), comma-separated for multiple | -| 4 | contact | No | String | Contact email (associated with first author) | -| 5 | supervisors | No | String | Supervisor(s), comma-separated for multiple | -| 6 | formats | No | String | Format(s), comma-separated for multiple | -| 7 | year | **Yes** | Integer | Year of thesis (e.g., 2024) | -| 8 | ap | No | String | AP program code (see AP Codes section) | -| 9 | orientation | No | String | Orientation code (see Orientation Codes section) | -| 10 | finality | No | String | Finality name | -| 11 | keywords | No | String | Keywords, comma-separated (max 10) | -| 12 | synopsis | No | Text | Synopsis/abstract of the thesis | -| 13 | context | No | Text | Context note | -| 14 | remarks | No | Text | Additional remarks | -| 15 | language | No | String | Language (e.g., Français, English, Nederlands) | -| 16 | access | No | String | Access authorization | -| 17 | license | No | String | License information | -| 18 | size_info | No | String | File size information | -| 19 | jury_points | No | Float | Jury score (out of 20) | -| 20 | baiu_link | No | String | Link to BAIU (institutional archive) | +- **Encoding:** UTF-8 +- **Delimiter:** comma (`,`), quoting with double quotes (`""` escapes a quote) +- **Header detection:** the importer scans up to **8 rows** looking for a header + row containing at least **11 recognised column names**. Matching is flexible: + prefix/substring/variant matching (e.g. `contact.visible` matches `contact`, + `Licence` ↔ `License`, `Promoteur·ice(s) université` matches `ulb`). +- **No header fallback:** if no header row is found, the importer falls back to + **positional** interpretation, skipping the first **4 rows** (the legacy + template layout with label/description rows). -## Field Details +So import files are robust to column reordering as long as meaningful headers +are present; legacy fixed-position files (skip-4) also still work. -### Required Fields -- **title**: Must not be empty -- **year**: Must not be empty and must be a valid integer +--- -### Multi-Value Fields -These fields accept multiple values separated by commas: -- **authors**: e.g., `"John Doe, Jane Smith"` -- **supervisors**: e.g., `"Prof. A, Prof. B"` -- **keywords**: Maximum 10 keywords, e.g., `"art, design, digital"` -- **formats**: e.g., `"PDF, Video, Installation"` +## Recognised columns -### Orientation Codes -Valid orientation codes and their full names: +Headings are matched by the keywords below (case-insensitive). They mirror the +**export** header names in `ExportController::CSV_HEADERS`. + +| Export header (order in export) | Imported as | Notes | +|---|---|---| +| `Identifiant` | identifier | Optional; if it already exists, the row is skipped | +| `Titre` | title | **Required** | +| `Sous-titre` | subtitle | | +| `Auteur·ice(s)` | authors | comma-separated; first author gets `Contact` | +| `Contact` | contact | email; `OUI`/`NON` artefacts emptied | +| `Promoteur·ice(s) interne` | promoteur (interne) | role `promoteur`, `is_ulb=0` | +| `Lecteur·ice(s) interne` | lecteur (interne) | role `lecteur`, `is_external=0` | +| `Lecteur·ice(s) externe` | lecteur (externe) | role `lecteur`, `is_external=1` | +| `Promoteur·ice(s) université` | promoteur (ulb) | role `promoteur`, `is_ulb=1` | +| `Format(s)` | formats | comma-separated; matched to `format_types` | +| `Année` | année | **Required** unless derivable from `Identifiant` (`2024-003` → 2024) | +| `AP` | ap | AP program code or name | +| `Orientation` | orientation | code or full name (see below) | +| `Finalité` | finalité | matched to `finality_types` | +| `Mots-clés` | mots-clés | comma-separated; lowercase-normalised; max 10 | +| `Synopsis` | synopsis | | +| `Contexte` | contexte | context note | +| `Remarques` | remarques | | +| `Langue` | langue | comma-separated; stored lowercase; created if missing | +| `Autorisation` | autorisation | access type name → `access_types`; default `Libre` if unknown | +| `Licence` / `License` | license | licence name | +| `Points sur 20` | points | float (jury points) | +| `Lien BAIU` | lien baiu | institutional link | +| `CC2r`, `Exemplaire BAIU`, `Exemplaire ERG` | *(export only)* | written by export; ignored on import | + +> Positional-fallback indices (0-based): `0` Identifier, `1` Titre, `2` +> Sous-titre, `3` Auteur, `4` Contact, `5` Promoteurs, `6` Lecteurs internes, +> `7` Lecteurs externes, `8` Promoteurs ULB, `9` Formats, `10` Année, `11` AP, +> `12` Orientation, `13` Finalité, `14` Mots-clés, `15` Synopsis, `16` Contexte, +> `17` Remarques, `18` Langue, `19` Autorisation, `20` Licence, `21` Points, +> `22` Lien BAIU. + +The three trailing **export-only** columns (CC2r / Exemplaire BAIU / Exemplaire +ERG) are not currently read back on import. + +--- + +## Orientation & AP values + +Orientation codes (legacy short form) and aliases are normalised to canonical +DB names. Recognised codes: ``` -SC = Sculpture -VI = Vidéographie -CA = Cinéma d'animation -IP = Installation-Performance -PE = Peinture -PH = Photographie -DE = Dessin -AN = Arts Numériques -GR = Graphisme -TY = Typographie -DN = Design Numérique -IL = Illustration -BD = Bande-Dessinée -SE = Sérigraphie -GV = Gravure +AN=Arts Numériques, DE=Dessin, CA=Cinéma d'animation, IP=Installation-Performance, +PE=Peinture, PH=Photographie, SC=Sculpture, VI=Vidéographie, GR=Graphisme, +TY=Typographie, DN=Design Numérique, IL=Illustration, BD=Bande-Dessinée, +SE=Sérigraphie, GV=Gravure ``` -### AP Codes -Valid AP program codes: -- `DPM` -- `LIENS` -- `APS` +AP programs (by code or name): `NS` Narration Spéculative, `DPM` Design et +Politique du Multiple, `APS` Atelier Pratiques Situées, `LIENS`, `PACS`. -(These codes must match exactly what exists in the `ap_programs` table) +--- -### Language Values -Languages should be provided with capital first letter: -- `Français` -- `English` -- `Nederlands` -- etc. +## Import behaviour -### Format Values -Common format values (case-insensitive, will be normalized): -- `PDF` -- `Video` -- `Audio` -- `Installation` -- `Web` -- etc. +- **Transaction per row** — a failed row is rolled back and logged; import + continues. +- **Required:** `Titre` and `Année` (year may be derived from the identifier). +- Empty rows (no title and no identifier) are skipped. +- Authors, supervisors, languages and tags are auto-created if missing. +- **Tag normalisation:** `strtolower`, collapse multiple spaces, de-duplicate, + cap at 10. +- Duplicate `Identifiant` → row skipped (not re-inserted). +- Results summary reports: imported count, skipped count, and per-row + `✓`/`✗` messages. -## Import Behavior +--- -### Row Processing -1. Empty rows (no title and no identifier) are skipped -2. Each row is processed in a transaction -3. If a row fails, it is skipped and logged, but processing continues +## Restore procedure -### Data Validation -- If title or year is missing, the row is rejected -- Invalid orientation codes result in no orientation being set (null) -- Invalid AP codes result in no AP program being set (null) -- Keywords are limited to first 10 if more are provided - -### Data Normalization -- All string fields are trimmed of whitespace -- Language and format values are normalized (first letter capitalized, rest lowercase) -- Empty strings are converted to NULL in the database - -### Entity Creation -- Authors, supervisors, and keywords are automatically created if they don't exist -- Existing authors are matched by name -- Contact email is only associated with the first author - -## Example CSV Structure - -```csv - -Identifiant,Titre,Sous-titre,Auteur·ice(s),Contact,Promoteur·ice(s),Format,Année,AP,Orientation,Finalité,Mots-clés,Synopsis,Contexte,Remarques,Langue,Autorisation,License,taille,Points sur 20,lien BAIU - -TFE-2024-001,Mon projet artistique,Exploration du numérique,"Alice Dupont, Bob Martin",alice@example.com,Prof. Smith,PDF,2024,DPM,AN,Création,art numérique,digital art,interactive installation,Un projet explorant l'intersection de l'art et de la technologie,Réalisé dans le cadre du master,Très bon projet,Français,Public,CC-BY,250MB,16.5,https://baiu.example.org/12345 -TFE-2024-002,Design graphique moderne,,Charlie Brown,charlie@example.com,"Prof. A, Prof. B","PDF, Print",2024,LIENS,GR,Design,typographie,graphisme,design,Une exploration de la typographie contemporaine,,,English,Restricted,All rights reserved,50MB,15, -``` - -## Troubleshooting - -### Common Issues -1. **Encoding problems**: Ensure file is saved as UTF-8 -2. **Missing columns**: All 21 columns must be present, even if empty -3. **Line breaks in fields**: Ensure fields containing newlines are properly quoted -4. **Quote escaping**: Use double quotes (`""`) to escape quotes within fields - -### Import Results -After import, the system will display: -- Number of theses successfully imported -- Number of rows skipped due to errors -- Detailed line-by-line results with success (✓) or error (✗) indicators - -## Notes - -- The import process preserves the order of authors, supervisors, and keywords -- The first author gets the contact email if provided -- Duplicate detection is not performed - each import creates new entries -- Failed rows do not stop the import process -- All errors are logged to the server error log +Full restore combines the three exports (CSV + DB + files). See +[export.md](export.md) for the step-by-step restore of the database and files; +the CSV is re-importable as described above when rebuilding from scratch. diff --git a/docs/search.md b/docs/search.md index 7d66c6a..6a01fb3 100644 --- a/docs/search.md +++ b/docs/search.md @@ -1,172 +1,98 @@ -# Search Feature Documentation +# Search & Répertoire — Documentation -## Overview -The search feature allows users to search across theses using multiple criteria including full-text search and advanced filters. +Two public browsing surfaces, both handled by `app/src/Controllers/SearchController.php` +and routed by `app/src/Dispatcher.php`: -## Files Created/Modified +| Route | Handler | Purpose | +|-------|---------|---------| +| `/search`, `/search.php` | `handleSearch()` | Full-text query + classic single filters | +| `/repertoire`, `/repertoire.php` | `handleRepertoire()` | Browseable directory with multi-select filters | -### New Files -1. **search.php** - Main search interface page -2. **create_test_db.php** - Script to generate test database with sample data -3. **search.md** - This documentation file +Both only ever expose **published** theses (`is_published = 1`). -### Modified Files -1. **Database.php** - Added search methods: - - `searchTheses()` - Search with multiple filters - - `countSearchResults()` - Count matching results - - `getAvailableYears()` - Get all years from published theses - - `getOrientations()` - Get all orientations - - `getApPrograms()` - Get all AP programs - - `getFinalityTypes()` - Get all finality types - - `getUsedKeywords()` - Get keywords used in published theses - - `getFormatTypes()` - Get all format types - - `getLanguages()` - Get all languages +--- -2. **inc/header.php** - Added "Rechercher" link to navigation +## HandleSearch (`/search`) -## Searchable Fields +`handleSearch()` reads from `$_GET` and renders `app/templates/public/search.php` +with the results fragment (`app/templates/partials/search-results.php`). -The search feature allows filtering by: +Searchable text fields (via `Database::searchTheses()`): +- Title, subtitle, synopsis, author names, supervisor names, tags/keywords. -1. **Full-text query** - Searches across: - - Title - - Subtitle - - Synopsis - - Author names - - Supervisor names - - Keywords +Single-value filters (`collectSearchParams()`): +- `query` — free text +- `year` — exact year +- `orientation` — artistic orientation +- `ap_program` — AP program +- `finality` — finality type +- `format` — format +- `keyword` — tag/keyword -2. **Year** - Filter by specific year +Results are paginated (`limit = 20` default); the **search bar** +(`app/templates/partials/search-bar.php`) submits a GET form to `/search`. -3. **Orientation** - Filter by artistic orientation: - - Arts Numériques, Dessin, Cinéma d'animation, Installation-Performance - - Peinture, Photographie, Sculpture, Vidéographie - - Graphisme, Typographie, Design Numérique, Illustration - - Bande-Dessinée, Sérigraphie, Gravure +--- -4. **AP Program** - Filter by atelier pratique: - - Narration Spéculative - - Design et Politique du Multiple (DPM) - - Atelier Pratiques Situées (APS) - - Lieux, Interdisciplinarités, Écologie, Nécessité, Systèmes (LIENS) +## HandleRepertoire (`/repertoire`) -5. **Finality** - Filter by master finality: - - Approfondi - - Enseignement - - Spécialisé +`handleRepertoire()` reads multi-select filter arrays from `$_GET` and renders +`app/templates/public/repertoire.php`, which uses the shared results partial +and `app/templates/partials/repertoire-index.php`. -6. **Format** - Filter by work format: - - Site web, Audio, Vidéo, Performance - - Objet éditorial, Installation, Autre +Multi-select filters (`collectFilterParams()`, each an array, `_GET` keys): +- `fy[]` — years (validated to 1900–2100) +- `ap[]` — AP program names +- `or[]` — orientations +- `fi[]` — finalities +- `kw[]` — keywords/tags -7. **Language** - Filter by language (Français, Anglais) +Each value is trimmed, length-capped (≤ 100), de-duplicated, and passed through +as sanitised strings — no direct user input reaches SQL. -8. **Keyword** - Filter by specific keyword +There is also an HTMX **student preview** popover at +`/repertoire/student-preview` (`handleStudentPreview()` → `student-preview.php`). -9. **Type** - Filter by thesis type: - - TFE (final thesis projects) - - Doctoral theses +--- -## Testing the Search Feature +## Rate limiting -### 1. Create Test Database -Run the script to generate sample data: -```bash -cd /home/padlock/dev/posterg-website/front-backend -php create_test_db.php -``` +Search is rate-limited via `app/src/RateLimit.php`. See the nginx config +(`nginx/xamxam.conf`) for the matching server-side limits. -This will create `test.db` in the `formulaire/` directory with: -- 6 sample theses (various years, orientations, and programs) -- 5 sample authors -- 3 sample supervisors -- 20 keywords -- Complete relationships (authors, supervisors, keywords, formats, languages) +--- -### 2. Access the Search Page -Navigate to: `search.php` +## Database access -### 3. Test Search Scenarios +- **Full-text + single filters:** `Database::searchTheses(array $params, $limit, $offset)` + and `Database::countSearchResults(array $params)`. +- **Keyword/tag autocomplete:** `Database::searchTags(string $query)`. +- **Supervisor autocomplete:** `Database::searchSupervisors($query, $role)`. +- **Language autocomplete:** `Database::searchLanguages(string $query)`. -#### Scenario 1: Full-text Search -- Enter "urbain" in the search field -- Should find: "Espaces Urbains et Narration Collective" +Queries operate on `v_theses_public`; keyword matching joins the `thesis_tags` / +`tags` tables (keywords are stored as lowercase-normalised **tags**, not a +`keywords`/`thesis_keywords` set — see [database.md](database.md)). -#### Scenario 2: Filter by Year -- Select year: 2024 -- Should find: 3 theses from 2024 +All queries use PDO prepared statements and escape `%`/`_` for `LIKE` +(`Database::escapeLikeString`) to prevent wildcard injection. -#### Scenario 3: Filter by Orientation -- Select orientation: "Installation-Performance" -- Should find: 2 theses +--- -#### Scenario 4: Filter by AP Program -- Select AP: "Narration Spéculative" -- Should find: 2 theses +## Performance notes -#### Scenario 5: Combined Filters -- Enter "performance" in search field -- Select year: 2024 -- Should find: 1 thesis ("Corps et Technologies") +- Critical text/filter columns and the junction tables are indexed + (`idx_theses_pub_year`, `idx_theses_*`, `idx_thesis_tags_*`, …). +- `v_theses_public` pre-computes the joins for the common read path. +- The repertoire filters operate on indexed lookup columns. -#### Scenario 6: Keyword Search -- Select keyword: "écologie" -- Should find: "Écologies Affectives" +--- -## Database Schema Reference +## Future enhancements (not yet implemented) -The search uses the `v_theses_public` view which combines: -- Main thesis data from `theses` table -- Related authors via `thesis_authors` junction table -- Related supervisors via `thesis_supervisors` junction table -- Related keywords via `thesis_keywords` junction table -- Related formats via `thesis_formats` junction table -- Related languages via `thesis_languages` junction table -- Predefined values from lookup tables (orientations, ap_programs, finality_types, etc.) +The historical `search.md` listed potential automplete/faceted-search/export +ideas. Status: -## Features - -### Pagination -- Results are paginated (20 items per page) -- Previous/Next navigation -- Numbered page links - -### Result Display -- Shows total number of results -- Card-based layout matching the main index page -- Displays: title, author, year, synopsis excerpt -- Links to full thesis detail page - -### User Experience -- All filters are optional -- Filters can be combined -- "Réinitialiser" button to clear all filters -- Maintains filter state during pagination - -## Security Considerations - -- All user inputs are sanitized using `htmlspecialchars()` -- SQL queries use prepared statements with parameter binding -- No direct SQL injection risk -- Only published theses are searchable (`is_published = 1`) - -## Future Enhancements - -Potential improvements: -1. **Auto-complete** - Suggest keywords/authors as user types -2. **Faceted search** - Show filter counts (e.g., "Peinture (12)") -3. **Sort options** - Sort by year, title, relevance -4. **Save searches** - Allow users to bookmark search queries -5. **Export results** - Export search results as CSV/JSON -6. **Advanced boolean search** - Support AND/OR/NOT operators -7. **Search highlights** - Highlight matching terms in results -8. **Related theses** - Show similar works based on keywords -9. **Statistics** - Show search analytics and popular queries -10. **AJAX search** - Live search without page reload - -## Technical Notes - -- Uses SQLite LIKE operator for text matching (case-insensitive) -- Searches across GROUP_CONCAT fields in the view for many-to-many relationships -- Efficient use of indexes defined in schema.sql -- Compatible with existing Database.php singleton pattern +- Auto-complete for tags exists at the form level (`searchTags`) +- Faceted counts, saved searches, result export, and advanced boolean operators + are **not** implemented diff --git a/docs/security.md b/docs/security.md index a8d32c7..0f35419 100644 --- a/docs/security.md +++ b/docs/security.md @@ -1,82 +1,95 @@ # Security -Vulnerability analysis and resolution status for posterg-website. +Current security posture for XAMXAM. -> Based on security audit (2026-02-08). All items tracked below. +> This supersedes the earlier `security.md` (2026-02-08 audit). The original +> 16-item audit is closed; the items below reflect the current state. --- -## Resolved +## Authentication — admin -### Infrastructure / Deployment +- **PHP session auth:** `app/src/AdminAuth.php`. Password-only (no username). + Credentials previously in a gitignored PHP file; the current build stores the + bcrypt hash in `site_settings.admin_password_hash` (manageable from + `/admin/account`). +- `AdminAuth::requireLogin()` guards every admin action/route. +- Session cookies hardened: `HttpOnly`, `SameSite=Strict`, `Secure`, + `Path=/admin`; regenerated on login. +- nginx `auth_basic` layer has been removed; the PHP session layer is the only + gate. (LDAP-based login is a proposed future enhancement — see + `LDAP_AUTH_PLAN.md` / `LDAP_SPEC.md`. It is **not** implemented.) -| # | Issue | Severity | Resolution | -|---|-------|----------|------------| -| 1 | No HTTPS — admin credentials exposed in transit | 🔴 CRITICAL | TLS terminated upstream by reverse proxy. nginx.conf doesn't need to handle TLS directly. | -| 3 | Uploaded files stored inside webroot | 🟠 HIGH | Storage moved to `STORAGE_ROOT` (`/var/www/posterg/storage/`), defined in `config/bootstrap.php`. | -| 4 | File path mismatch — media broken & insecure | 🟠 HIGH | DB paths now storage-relative. New `public/media.php` serves files safely. `memoire.php` and `search.php` use `/media.php?path=…`. Cover recording fixed. | -| 5 | Rate limiter bypassed by IP spoofing (`X-Forwarded-For`) | 🟠 HIGH | `src/RateLimit.php` `getClientIdentifier()` uses `REMOTE_ADDR` only. | -| 6 | `.htaccess` rules silently ignored by nginx | 🟠 HIGH | All rules ported to `nginx/posterg.conf`. See `nginx/HTACCESS_TO_NGINX.md`. | -| 13 | Deprecated `X-XSS-Protection` header | 🔵 LOW | Removed from `nginx/posterg.conf`. | +## Transport & headers -### Frontend / Assets +Enforced in `nginx/xamxam.conf` (see `nginx/docs/SECURITY_HEADERS.md`): -| # | Issue | Severity | Resolution | -|---|-------|----------|------------| -| 10 | CDN stylesheet without SRI | 🟡 MEDIUM | CDN will not be used in production. Self-hosted, eliminating supply-chain risk. | +- **HSTS** (`Strict-Transport-Security`, 730 days, preload) +- **CSP** — `default-src 'self'; … frame-ancestors 'none'` on public pages; + `frame-ancestors 'self'` where the app embeds allowed content; admin CSP + includes `script-src 'unsafe-inline'` for the OverType editor. `object-src 'none'`. +- `X-Frame-Options: DENY` (clickjacking) +- `X-Content-Type-Options: nosniff` +- `Referrer-Policy: strict-origin-when-cross-origin` +- `Permissions-Policy` (geo/mic/camera disabled) +- `Cross-Origin-Opener-Policy` / `Cross-Origin-Resource-Policy: same-origin` +- `X-Robots-Tag: noindex, nofollow` on `/admin/` -### Code Quality / Defence in Depth +`X-XSS-Protection` is intentionally **omitted** (deprecated; see +`nginx/docs/SECURITY_HEADERS.md` for rationale). -| # | Issue | Severity | Resolution | -|---|-------|----------|------------| -| 14 | Missing `rel="noreferrer"` on external links | 🔵 LOW | `rel="noopener noreferrer"` applied in `public/admin/thanks.php`. | -| 15 | Unescaped integer outputs | 🔵 LOW | Explicit `(int)` casts added in `public/index.php` and `public/search.php`. | -| 16 | Redundant `DATABASE_PATH` constant | 🔵 LOW | Removed from `config/bootstrap.php`. | +## Rate limiting -### Admin Panel — Authentication & Sessions +Defined in the nginx config `limit_req_zone` and enforced by +`app/src/RateLimit.php`: -| # | Issue | Severity | Resolution | -|---|-------|----------|------------| -| 2 | No PHP-level authentication in admin | 🔴 CRITICAL | `src/AdminAuth.php` implements session guard with `password_verify` + `session_regenerate_id`. All admin files call `AdminAuth::requireLogin()`. Credentials in gitignored `config/admin_credentials.php`. No-op when constant absent (dev/cli-server). | -| 8 | Session cookies not hardened | 🟡 MEDIUM | Resolved with #2. `AdminAuth::startSession()` sets `HttpOnly=true`, `SameSite=Strict`, `Secure=true` (off on cli-server), `Path=/admin`, `Lifetime=0`. | +- General requests: `30 r/m` +- Search endpoint: `30 r/m` +- Admin panel: `300 r/m` (burst 30) ---- +The PHP limiter uses `REMOTE_ADDR` only (not `X-Forwarded-For`) to avoid IP +spoofing. -## In Progress +## Files & storage -| # | Issue | Severity | Status | -|---|-------|----------|--------| -| 7 | LIKE wildcard injection in admin search | 🟡 MEDIUM | Public `Database::searchTheses()` escapes `%` and `_` correctly. Same pattern must be applied to admin search and any other raw LIKE queries. | +- Uploads live **outside the webroot** under `app/storage/` (`tfe/`, `theses/`), + served on demand via `MediaController`/`FileAccessController` through + controlled endpoints (`/media`), not direct static access. +- nginx blocks `/storage`, `/src`, `/templates`, DB/sql/env/md files, and hidden + files. The DocumentRoot is `app/public/` only. +- Restricted-file downloads are gated by a request/approval/token flow + (`file_access_*` tables). +- Logs write to `app/storage/logs/` — outside the webroot, not publicly served. ---- +## Injection & output -## Not Yet Implemented +- **SQL:** all queries use PDO prepared statements. +- **LIKE wildcards:** `Database::escapeLikeString()` escapes `%` and `_` + (public search and related queries). +- **XSS:** `htmlspecialchars()` on all user-controlled output; integer/ID + inputs cast. +- **CSRF:** per-session tokens (`bin2hex(random_bytes(32))`), compared with + `hash_equals()`. +- **File uploads:** MIME validated (`finfo`); FilePond prevalidation + + server-side checks in `FilepondHandler`. +- **Markdown/HTML:** user content (pages, help blocks) rendered via + `league/commonmark`; HTML in admin-editable content is expected. -| # | Issue | Severity | Files | -|---|-------|----------|-------| -| 11 | Missing Content-Security-Policy on public pages | 🟡 MEDIUM | `nginx/posterg.conf` → add CSP header to main server block | -| 9 | `error.log` in web-accessible path | 🟡 MEDIUM | `public/admin/actions/formulaire.php` → use absolute path outside webroot | -| 12 | CSV import missing server-side MIME validation | 🟡 MEDIUM | `public/admin/import.php` → add `finfo` MIME check | +## Defence in depth / misc ---- +- External links use `rel="noopener noreferrer"`. +- Decryption/`Crypto` failures are logged without leaking secrets; SMTP + password is stored encrypted in `smtp_settings` and rotated via + `just reencrypt-password`. +- Admin operations are recorded in `admin_audit_log` (resource, action, + status, IP, User-Agent). -## Priority Order +## Areas to keep monitored -1. ~~🔴 CRITICAL~~ — All done (items 1–2) -2. 🟡 **MEDIUM** — Items 7, 9, 11, 12 remaining -3. ~~🔵 LOW~~ — All done (items 13–16) +- Tightening the public CSP (`frame-ancestors 'none'` vs `'self'` on embed + routes) is an active topic — see `TODO.md`. +- Sensitive file downloads and their expiry/token handling are worth periodic + review as usage grows. ---- - -## Good Practices Already in Place - -- ✅ SQL injection: all queries use PDO prepared statements -- ✅ XSS output: `htmlspecialchars()` on all user-controlled output -- ✅ CSRF: tokens with `bin2hex(random_bytes(32))`, validated with `hash_equals()` -- ✅ File upload: MIME type validated with `finfo` -- ✅ Input validation: year, IDs, pagination cast to integers -- ✅ LIKE wildcard escaping in public search (`Database::escapeLikeString`) - ---- - -*Last updated: 2026-02-08* +See also: `nginx/docs/SECURITY_HEADERS.md`, `nginx/docs/PHP_AUTH_LAYER.md`, +`nginx/docs/PRODUCTION_DEPLOYMENT.md`. diff --git a/nginx/README.md b/nginx/README.md index b15352d..284fc54 100644 --- a/nginx/README.md +++ b/nginx/README.md @@ -1,6 +1,6 @@ -# Nginx Configuration - Post-ERG +# Nginx Configuration - XAMXAM -This directory contains nginx configuration and documentation for the Post-ERG thesis website. +This directory contains nginx configuration and documentation for the XAMXAM TFE website. ## 📁 Files diff --git a/nginx/docs/SECURITY_HEADERS.md b/nginx/docs/SECURITY_HEADERS.md index d80be40..bfad2f4 100644 --- a/nginx/docs/SECURITY_HEADERS.md +++ b/nginx/docs/SECURITY_HEADERS.md @@ -5,8 +5,8 @@ | Header | Value | Purpose | |--------|-------|---------| | `Strict-Transport-Security` | `max-age=63072000; includeSubDomains; preload;` | HSTS — forces HTTPS | -| `Content-Security-Policy` | `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; frame-ancestors 'none';` | Restrict resource origins; block embedding | -| `X-Frame-Options` | `SAMEORIGIN` | Prevent clickjacking | +| `Content-Security-Policy` | `default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; frame-ancestors 'none';` | Restrict resource origins; block embedding (public pages) | +| `X-Frame-Options` | `DENY` | Prevent clickjacking | | `X-Content-Type-Options` | `nosniff` | Prevent MIME-type sniffing | | `Referrer-Policy` | `strict-origin-when-cross-origin` | Limit referrer leakage | | `Permissions-Policy` | `geolocation=(), microphone=(), camera=()` | Disable unused browser APIs | @@ -17,7 +17,7 @@ | Header | Value | Purpose | |--------|-------|---------| -| `Content-Security-Policy` | `default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; frame-ancestors 'none';` | Restrict resource origins; allows inline scripts for OverType editor | +| `Content-Security-Policy` | `default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; frame-ancestors 'none';` | Restrict resource origins; allows inline scripts for OverType editor | | `X-Robots-Tag` | `noindex, nofollow` | Prevent search-engine indexing of admin | These were previously declared in `public/admin/.htaccess` as Apache @@ -38,5 +38,5 @@ to expose response bodies that would otherwise be blocked. Sending it provides no protection and may introduce risk. **Correct mitigation:** a proper `Content-Security-Policy` header (now done for -`/admin/`; public-page CSP is todo item #11). +both `/admin/` and public pages; embed routes use `frame-ancestors 'self'`).