mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
feat: require 3 mots-clés in partage, language asterisk toggle, admin auto-save checkboxes
- tag-search: add minTags/required params, counter shows red if < 3, accent if ≥ 3 - form.php: pass minTags=3 for partage mode keywords - checkbox-list: support labelHtml for raw HTML label with targetable asterisk span - language-autre-fragment: OOB swap updates #languages-required-asterisk when autre pills change - language-search: client-side update #languages-required-asterisk on pill add/remove - contenus.php: replace 3 form+submit-button fieldsets with HTMX auto-save checkboxes - settings.php: detect HX-Request header, return OOB CSRF token updates, skip redirect
This commit is contained in:
91
docs/CURRENT_ISSUES.md
Normal file
91
docs/CURRENT_ISSUES.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# Current Issues — XAMXAM (2026-05-10)
|
||||
|
||||
## 1. FK constraint violation on thesis save (create + edit)
|
||||
|
||||
**Symptom:** `⚠ SQLSTATE[23000]: Integrity constraint violation: 19 FOREIGN KEY constraint failed`
|
||||
|
||||
**Triggers:**
|
||||
- Editing an imported CSV thesis, changing access type to Interdit, save
|
||||
- Opening any imported thesis edit form and saving *without changes*
|
||||
- Saving the add form with empty fields — form below "Cadre académique" disappears (PHP dies mid-render)
|
||||
|
||||
**Root cause found so far:** `Database::createThesis()` at line ~1860 was doing `(int)$data['orientation_id']` which converts SQL null → PHP `null` → `(int)null` = `0`. Since no row with ID 0 exists in `orientations`, this triggers FK violation. Fixed in commit `55088c94` by using `$v ? (int)$v : null` pattern.
|
||||
|
||||
**Still happening after fix** — suggests another code path has same issue, or the fix wasn't complete. The `updateThesis` path was already safe (uses `?: null`), but the error persists.
|
||||
|
||||
**Debugging added:**
|
||||
- `[DB:updateThesis]` log line with all FK values before query (commit `55088c94`)
|
||||
- `[ThesisEdit] Step 1-6 OK` step-level logging (commit `8734d964`)
|
||||
- `ErrorHandler::log()` with full trace on catch (commit `03ad73f3`)
|
||||
|
||||
**Dev server output:** No error_log visible in dev mode — PHP built-in server sends errors to stderr which may not be captured.
|
||||
|
||||
**Next steps:**
|
||||
- Enable `display_errors=1` in dev mode so FK errors render in browser
|
||||
- Check if `setThesisFormats`, `setThesisLanguages`, `setThesisTags` paths also have `(int)null` → `0` issues
|
||||
- Check `formulaire.php` action file — does it also use `createThesis`?
|
||||
|
||||
---
|
||||
|
||||
## 2. Dev server debugging output
|
||||
|
||||
**Symptom:** No error output visible in browser when PHP crashes.
|
||||
|
||||
**Current config** (`bootstrap.php`):
|
||||
- Dev mode (cli-server): `display_errors=1`, `error_reporting=E_ALL`
|
||||
- Production: `display_errors=0`, `log_errors=1`
|
||||
|
||||
**But:** the admin action files override this:
|
||||
- `formulaire.php` line 5-7: `ini_set('display_errors', 0); ini_set('log_errors', 1);`
|
||||
- `edit.php` action: no override (uses bootstrap defaults)
|
||||
|
||||
**Action needed:** Don't suppress display_errors in dev mode. Check `php_sapi_name()` before overriding.
|
||||
|
||||
---
|
||||
|
||||
## 3. Console warnings
|
||||
|
||||
```
|
||||
Layout was forced before the page was fully loaded. node.js:416:1
|
||||
[file-upload-queue] XamxamInitFileUploads called (twice)
|
||||
```
|
||||
- `file-upload-queue.js` called twice — the script might be included twice (check `add.php` template + the `form.php` partial)
|
||||
|
||||
---
|
||||
|
||||
## 4. Tags: lowercase + dedup + CSV import
|
||||
|
||||
**Status:** Implemented across all paths:
|
||||
- Frontend JS: `normalizeTag()` with `replace(/\s+/g, ' ')`, lowercase
|
||||
- Server fragment: `preg_replace('/\s+/', ' ', strtolower(...))`
|
||||
- Both controllers: `fn(string $t) => strtolower(trim(preg_replace('/\s+/', ' ', $t)))`
|
||||
- CSV import: same normalization (commit `8734d964`)
|
||||
- Minimum 3 tags enforced (commit `8734d964`)
|
||||
|
||||
## 5. ErrorHandler coverage
|
||||
|
||||
**Status:** Applied to 12 admin action files + 6 public controllers + 2 form controllers + partage entry point (commit `03ad73f3`). 77 unit test assertions.
|
||||
|
||||
## Relevant commits (most recent first)
|
||||
|
||||
```
|
||||
55088c94 Fix FK violation: (int)null → 0 in createThesis
|
||||
27378b42 ErrorHandler tests: 77 assertions
|
||||
4d9296fd ErrorHandler: precise FK field extraction from SQLite
|
||||
03ad73f3 ErrorHandler: shared logging across all actions/controllers
|
||||
6b6c62d1 Error logging: step-by-step transaction tracing
|
||||
8734d964 Mots-clés: collapse spaces, minimum 3 keywords
|
||||
dfe1186b Mots-clés: lowercase, dedup, keyboard nav, absolute dropdown
|
||||
8d04d4ba Mots-clés: lowercase enforcement, deduplication, absolute dropdown
|
||||
7fe53f8c Mots-clés: interactive HTMX tag search
|
||||
dd110cc5 Admin mobile block: fix inline style beating media query
|
||||
```
|
||||
|
||||
## Key files to review
|
||||
|
||||
- `app/src/Database.php` — `createThesis()` line ~1830, `updateThesis()` line ~1751, `setThesisFormats/Languages/Tags`
|
||||
- `app/src/Controllers/ThesisCreateController.php` — `submit()` line ~146, `validateAndSanitise()` line ~312
|
||||
- `app/src/Controllers/ThesisEditController.php` — `save()` line ~158
|
||||
- `app/public/admin/actions/formulaire.php` — calls `$ctrl->submit()` (create path)
|
||||
- `app/public/admin/actions/edit.php` — calls `$ctrl->save()` (edit path)
|
||||
- `app/public/admin/index.php` — CSV import at line ~220
|
||||
141
docs/LDAP_SPEC.md
Normal file
141
docs/LDAP_SPEC.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# LDAP Authentication Specification for XAMXAM Admin
|
||||
|
||||
## Current state
|
||||
|
||||
Two-layer authentication guards the `/admin/` area:
|
||||
|
||||
| Layer | Mechanism | Where |
|
||||
|-------|-----------|-------|
|
||||
| 1 (nginx) | `auth_basic` against `/etc/nginx/.htpasswd-xamxam` | `nginx/xamxam.conf` |
|
||||
| 2 (PHP) | `AdminAuth` — bcrypt hash in `site_settings.admin_password_hash` | `app/src/AdminAuth.php`, `app/public/admin/login.php`, `app/public/admin/actions/account.php` |
|
||||
|
||||
Layer 1 controls the browser's Basic Auth dialog. Layer 2 provides a PHP session gate and a
|
||||
fallback login form. When both layers share the same password, the user is authenticated
|
||||
transparently (nginx passes `PHP_AUTH_PW` to PHP, `AdminAuth` verifies it against the DB hash).
|
||||
|
||||
## Goal
|
||||
|
||||
Replace both layers with LDAP-based authentication while preserving the defence-in-depth
|
||||
structure and the transparent user experience (single sign-on via the browser's Basic Auth
|
||||
dialog, no PHP login form unless fallback).
|
||||
|
||||
## Required information from IT
|
||||
|
||||
| # | Item | Example / format |
|
||||
|---|------|------------------|
|
||||
| 1 | **LDAP server URL** | `ldaps://ldap.erg.be:636` or `ldap://ldap.erg.be:389` |
|
||||
| 2 | **Base DN** | `dc=erg,dc=be` |
|
||||
| 3 | **Bind DN** (service / search account) | `cn=svc-xamxam,ou=services,dc=erg,dc=be` |
|
||||
| 4 | **Bind password** | (secret — read-only account is sufficient) |
|
||||
| 5 | **User search filter** | `(&(uid=%s)(memberOf=cn=admin-xamxam,ou=groups,dc=erg,dc=be))` — `%s` is the username entered in the Basic Auth dialog |
|
||||
| 6 | **Group membership mechanism** | `memberOf` attribute (AD-style) **or** `member`/`uniqueMember` on the group entry (OpenLDAP-style) |
|
||||
| 7 | **Username attribute** | Typically `uid` (OpenLDAP) or `sAMAccountName` (AD). What attribute should the user type in the auth dialog? |
|
||||
| 8 | **TLS certificate** | If `ldaps://` is used and the certificate is self-signed, provide the CA certificate (PEM). Otherwise confirm it's a publicly-trusted cert. |
|
||||
| 9 | **Admin group DN/CN** | The exact DN or CN that grants admin access (e.g. `cn=xamxam-admins,ou=groups,dc=erg,dc=be`). If there's no group yet, what should it be named? |
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Browser Nginx LDAP daemon LDAP server
|
||||
│ │ │ │
|
||||
│─ GET /admin/ ──────────►│ │ │
|
||||
│◄── 401 WWW-Authenticate │ │ │
|
||||
│─ GET /admin/ + Basic ──►│ │ │
|
||||
│ │─ POST /auth-ldap ───────────►│ │
|
||||
│ │ (proxy Authorization hdr) │─ ldap_bind ──────────►│
|
||||
│ │ │◄── success ───────────│
|
||||
│ │ │─ ldap_search ────────►│
|
||||
│ │ │◄── group check OK ────│
|
||||
│ │◄── 200 OK ──────────────────│ │
|
||||
│ │─ forward to PHP ────────────► │
|
||||
│ │ │ │
|
||||
│◄── admin page ─────────│ │
|
||||
```
|
||||
|
||||
### Option A — `nginx-ldap-auth` daemon (preferred)
|
||||
|
||||
- Drop-in replacement for `auth_basic` / `.htpasswd` using nginx's `auth_request` module
|
||||
- A small Python 3 daemon (`nginx-ldap-auth`) runs at `127.0.0.1:8888`
|
||||
- Configured via `/etc/nginx-ldap-auth.conf` (JSON or YAML)
|
||||
- Nginx proxies the `Authorization` header to the daemon; daemon binds to LDAP,
|
||||
checks group membership, returns 200 or 403
|
||||
- **The PHP `AdminAuth` layer remains** — it receives `PHP_AUTH_PW` from nginx,
|
||||
can verify the username against LDAP group membership, and establish the PHP session
|
||||
|
||||
Nginx config (add to `location ^~ /admin/`):
|
||||
|
||||
```nginx
|
||||
location ^~ /admin/ {
|
||||
# Replace auth_basic + auth_basic_user_file with:
|
||||
auth_request /auth-ldap;
|
||||
auth_request_set $saved_set_cookie $upstream_http_set_cookie;
|
||||
add_header Set-Cookie $saved_set_cookie;
|
||||
|
||||
# Client-facing Basic Auth challenge (so the browser asks for credentials)
|
||||
satisfy any;
|
||||
# Fallback: if auth_request returns 401, challenge
|
||||
error_page 401 = @ldap_challenge;
|
||||
|
||||
# Keep: rate limiting, CSP, PHP handling, security headers
|
||||
limit_req zone=admin burst=20 nodelay;
|
||||
# ... rest as-is ...
|
||||
}
|
||||
|
||||
# Internal endpoint — delegates to LDAP daemon
|
||||
location = /auth-ldap {
|
||||
internal;
|
||||
proxy_pass http://127.0.0.1:8888;
|
||||
proxy_pass_request_body off;
|
||||
proxy_set_header Content-Length "";
|
||||
proxy_set_header Authorization $http_authorization;
|
||||
}
|
||||
|
||||
# Trigger browser Basic Auth dialog when LDAP returns 401
|
||||
location @ldap_challenge {
|
||||
add_header WWW-Authenticate 'Basic realm="Admin Access - XAMXAM"';
|
||||
return 401;
|
||||
}
|
||||
```
|
||||
|
||||
### Option B — `ngx_http_auth_ldap_module` (native nginx module)
|
||||
|
||||
- Requires recompiling nginx with this third-party module
|
||||
- Simpler config: `auth_ldap "XAMXAM Admin"; auth_ldap_servers { ... }`
|
||||
- Less flexible; harder to debug
|
||||
|
||||
### Option C — PHP-only LDAP (no nginx layer)
|
||||
|
||||
- Remove nginx auth entirely
|
||||
- `AdminAuth::requireLogin()` does `ldap_bind()` + group check directly in PHP
|
||||
- Simpler nginx config, but no nginx-level gate
|
||||
- Browser auth dialog still possible via PHP sending `WWW-Authenticate` header
|
||||
|
||||
## After LDAP is working: cleanup checklist
|
||||
|
||||
| Step | File(s) affected | Action |
|
||||
|------|-----------------|--------|
|
||||
| 1 | `app/src/AdminAuth.php` | Remove `getStoredHash()`, `setPasswordHash()`, `removePasswordHash()`, `hasPassword()`, `verifyHash()`. Keep `requireLogin()`, `isAuthenticated()`, `login()`, `logout()` — adapt them to LDAP group check. |
|
||||
| 2 | `app/public/admin/login.php` | Remove entirely (no more PHP login form). |
|
||||
| 3 | `app/public/admin/actions/account.php` | Remove entirely (no more password CRUD). |
|
||||
| 4 | `app/templates/admin/login.php` | Remove template file. |
|
||||
| 5 | `app/templates/admin/parametres.php` | Remove the "Compte administrateur" `<section>` (password set/change/delete UI). |
|
||||
| 6 | `app/public/admin/parametres.php` | Remove `AdminAuth::hasPassword()` call and related variables. |
|
||||
| 7 | `app/templates/admin/account.php` | Remove if only used for password management. |
|
||||
| 8 | `nginx/xamxam.conf` | Remove `auth_basic` and `auth_basic_user_file` lines from the admin location block. |
|
||||
| 9 | Database | Remove `admin_password_hash` row from `site_settings` table (manual or migration). |
|
||||
| 10 | `app/bootstrap.php` | Remove legacy `ADMIN_PASSWORD_HASH` constant reference if present. |
|
||||
|
||||
## Dependencies to install
|
||||
|
||||
- **Option A**: Python 3, `python3-ldap` (or `pip install python-ldap`), `nginx-ldap-auth` daemon
|
||||
- **Option B**: nginx recompiled with `ngx_http_auth_ldap_module`
|
||||
- **Option C**: PHP `ldap` extension (`php8.4-ldap` or `apt install php-ldap`)
|
||||
|
||||
## Notes
|
||||
|
||||
- The `AdminAuth` PHP layer should remain even after LDAP is implemented — it provides
|
||||
session persistence, logout, CSRF integration, and the admin audit log identity.
|
||||
- The LDAP daemon/nginx layer handles **authentication** (who are you?).
|
||||
The PHP `AdminAuth` layer handles **session management** (are you still you?).
|
||||
- If IT provides a dedicated admin group, access control is centralised: adding/removing
|
||||
an admin is a single LDAP operation, no need to touch the server.
|
||||
Reference in New Issue
Block a user