Refactor admin panel and add migration documentation

- Add comprehensive migration guides (DEPLOYMENT_MIGRATION.md, DIRECTORY_STRUCTURE.md, MIGRATION_CHECKLIST.md)
- Refactor admin panel: split add.php, create reusable header/footer
- Update styles: admin.css, common.css, main.css
- Improve public pages: index.php, memoire.php
- Reorganize database documentation into database/docs/
- Update .gitignore and justfile

This prepares for migration to public/ directory structure
This commit is contained in:
Théophile Gervreau-Mercier
2026-02-06 11:33:20 +01:00
parent d2b3c6ca67
commit e789c286de
24 changed files with 2365 additions and 1125 deletions

20
.gitignore vendored
View File

@@ -1,27 +1,13 @@
# Vendor directory (third-party code)
vendor/ vendor/
compose.lock compose.lock
### Test databases ### ### Test databases ###
formulaire/test.db database/test.db
db/test.db
### Data et Mémoire###
formulaire/data/yaml/*
formulaire/data/content/*
formulaire/data/cover/*
formulaire/data/theses/*
formulaire/data/covers/*
front-backend/data/yaml/*
front-backend/data/content/*
front-backend/data/cover/*
### Logs ### ### Logs ###
formulaire/error.log formulaire/error.log
lib/cache/rate_limit/
# Vendor directory (third-party code)
vendor/
# OS files # OS files
.DS_Store .DS_Store

571
DEPLOYMENT_MIGRATION.md Normal file
View File

@@ -0,0 +1,571 @@
# Deployment & Dev Server Migration Guide
Analysis of current `justfile` and required changes for the new directory structure.
---
## 🔍 Current Setup Analysis
### Current Dev Server (❌ INCORRECT)
```bash
# From justfile line 33
php -S 127.0.0.1:8000
```
**Problems:**
- Serves from project root (all files accessible via web)
- Exposes sensitive files: `database/`, `tests/`, `vendor/`, config files
- Doesn't match production DocumentRoot configuration
- Security risk: `.env`, database files, source code all accessible
### Current Deployment (❌ INCORRECT)
```bash
# From justfile lines 56-75
rsync -vur --progress \
--exclude 'vendor' --exclude 'tests' --exclude '*.db' ... \
./ posterg:/var/www/html/
```
**Problems:**
- Deploys entire project to DocumentRoot
- Relies on exclusions to hide sensitive files (error-prone)
- Wrong structure: `/var/www/html/` should only contain public files
- Private files (`src/`, `config/`) are still in DocumentRoot
- Nginx serves from `/var/www/html/` exposing everything not excluded
---
## ✅ Required Changes
### 1. New Directory Structure on Server
**Before (current):**
```
/var/www/html/ # DocumentRoot ❌
├── index.php # Public
├── search.php # Public
├── admin/ # Public (protected by nginx)
├── assets/ # Public
├── inc/ # ❌ EXPOSED (config files!)
├── lib/ # ❌ EXPOSED (source code!)
├── database/ # ❌ EXPOSED (database files!)
└── vendor/ # ❌ (excluded but in wrong place)
```
**After (recommended):**
```
/var/www/posterg/ # Application root (private)
├── public/ # DocumentRoot ✅ (only this exposed)
│ ├── index.php
│ ├── search.php
│ ├── memoire.php
│ ├── admin/
│ └── assets/
├── src/ # ✅ PRIVATE
├── config/ # ✅ PRIVATE
├── database/ # ✅ PRIVATE
├── vendor/ # ✅ PRIVATE
├── var/ # ✅ PRIVATE (cache, logs)
└── lib/ # ✅ PRIVATE
```
---
## 📝 Updated Justfile
### Change 1: Dev Server
**Current:**
```just
[group('dev')]
serve:
@echo "🚀 Starting Post-ERG development server"
@echo "📍 Public site: http://localhost:8000"
@echo "📍 Admin panel: http://localhost:8000/admin/"
@php -S 127.0.0.1:8000
```
**New:**
```just
[group('dev')]
serve:
@echo "🚀 Starting Post-ERG development server"
@echo "========================================"
@echo ""
@echo "📍 Public site: http://localhost:8000"
@echo "📍 Admin panel: http://localhost:8000/admin/"
@echo ""
@echo "🔒 Serving from public/ directory (matches production)"
@echo ""
@if [ -d "vendor/php-live-reload" ]; then \
echo "✨ Live reload enabled - browser auto-refreshes on file save!"; \
else \
echo "💡 Tip: Run 'just setup' to enable live reload"; \
fi
@echo ""
@echo "Press Ctrl+C to stop"
@echo ""
@php -S 127.0.0.1:8000 -t public/
# Alternative: If you need router script for URL rewriting
[group('dev')]
serve-router:
@echo "🚀 Starting with router script (for clean URLs)"
@php -S 127.0.0.1:8000 -t public/ public/router.php
```
**Key change:** `-t public/` flag tells PHP server to use `public/` as DocumentRoot
---
### Change 2: Deployment Strategy
**Current (❌):**
```just
deploy:
rsync -vur --progress \
--exclude 'vendor' --exclude 'tests' ... \
./ posterg:/var/www/html/
```
**New (✅) - Two-Step Deployment:**
```just
[group('deploy')]
deploy:
@echo "📤 Deploying Post-ERG site"
@echo "=========================="
@echo ""
@echo "Step 1: Deploying application files..."
# Deploy entire application to private directory
rsync -vur --progress \
--exclude '.git*' \
--exclude '.jj' \
--exclude 'tests/' \
--exclude 'docs/' \
--exclude '*.md' \
--exclude 'justfile*' \
--exclude 'setup-dev.sh' \
--exclude 'migrate-structure.sh' \
--exclude 'database/test.db' \
--exclude 'database/backup_*' \
--exclude 'database/fixtures/' \
--exclude 'var/cache/*' \
--exclude 'var/logs/*' \
--exclude '.DS_Store' \
./ posterg:/var/www/posterg/
@echo ""
@echo "Step 2: Setting up directory structure..."
# Create necessary directories on server
ssh posterg "mkdir -p /var/www/posterg/var/{cache,logs,tmp} && \
chown -R www-data:posterg /var/www/posterg/var && \
chmod -R 775 /var/www/posterg/var"
@echo ""
@echo "Step 3: Setting permissions..."
# Set correct ownership and permissions
ssh posterg "cd /var/www/posterg && \
chown -R www-data:posterg . && \
find . -type d -exec chmod 755 {} \; && \
find . -type f -exec chmod 644 {} \; && \
chmod -R 775 var/ && \
chmod -R 775 database/ && \
chmod 660 database/*.db"
@echo ""
@echo "✅ Deployment complete!"
@echo ""
@echo "🔍 Verify deployment:"
@echo " • Public: https://posterg.erg.be/"
@echo " • Admin: https://posterg.erg.be/admin/"
@echo ""
@echo "📁 Server structure:"
@echo " • App root: /var/www/posterg/"
@echo " • DocumentRoot: /var/www/posterg/public/"
# Quick deploy - only public files (faster for frontend changes)
[group('deploy')]
deploy-public:
@echo "⚡ Quick deploy: public files only"
rsync -vur --progress \
--delete \
./public/ posterg:/var/www/posterg/public/
@echo "✅ Public files updated"
# Deploy only code (no assets)
[group('deploy')]
deploy-code:
@echo "⚡ Deploying PHP code only"
rsync -vur --progress \
--include='**.php' \
--include='**/' \
--exclude='*' \
./ posterg:/var/www/posterg/
@echo "✅ Code updated"
```
---
### Change 3: Database Deployment
**Current:**
```just
test-deploy:
ssh posterg "mkdir -p /var/www/html/database"
rsync -vur --progress ./database/test.db posterg:/var/www/html/database/test.db
```
**New:**
```just
[group('deploy')]
deploy-database:
@echo "📊 Deploying database..."
@echo "⚠️ This will overwrite the remote database!"
@read -p "Continue? [y/N] " -n 1 -r; \
echo; \
if [[ $$REPLY =~ ^[Yy]$$ ]]; then \
ssh posterg "mkdir -p /var/www/posterg/database" && \
rsync -vur --progress ./database/test.db posterg:/var/www/posterg/database/ && \
ssh posterg "chown www-data:posterg /var/www/posterg/database/test.db && \
chmod 660 /var/www/posterg/database/test.db" && \
echo "✅ Database deployed"; \
else \
echo "❌ Cancelled"; \
fi
# Backup remote database before deploying
[group('deploy')]
backup-remote-db:
@echo "💾 Backing up remote database..."
@ssh posterg "sqlite3 /var/www/posterg/database/test.db .dump" > database/remote_backup_$(date +%Y%m%d_%H%M%S).sql
@echo "✅ Remote database backed up locally"
```
---
### Change 4: Updated Nginx Deployment
**Current nginx configuration probably points to:**
```nginx
root /var/www/html;
```
**Should change to:**
```nginx
root /var/www/posterg/public;
```
**Updated recipe:**
```just
[group('server')]
deploy-nginx:
@echo "🔧 Deploying nginx configuration..."
@echo ""
@echo "⚠️ IMPORTANT: Nginx config must point to /var/www/posterg/public"
@echo ""
# Check if nginx config has correct DocumentRoot
@if ! grep -q "/var/www/posterg/public" nginx/posterg.conf 2>/dev/null; then \
echo "❌ ERROR: nginx/posterg.conf doesn't contain '/var/www/posterg/public'"; \
echo " Update DocumentRoot before deploying!"; \
exit 1; \
fi
rsync -vur --progress ./nginx/posterg.conf posterg:/tmp/posterg.conf
rsync -vur --progress ./nginx/deploy-production.sh posterg:/tmp/deploy-production.sh
@echo "✅ Files uploaded to /tmp/ on server"
@echo ""
@echo "Next steps on the server:"
@echo " ssh posterg"
@echo " sudo bash /tmp/deploy-production.sh"
```
---
## 🔧 Nginx Configuration Changes
### Update `nginx/posterg.conf`
**Find and replace:**
```nginx
# OLD
root /var/www/html;
# NEW
root /var/www/posterg/public;
```
**Complete example:**
```nginx
server {
listen 80;
listen [::]:80;
server_name posterg.erg.be;
# Redirect to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name posterg.erg.be;
# NEW DocumentRoot - only public directory
root /var/www/posterg/public;
index index.php index.html;
# SSL configuration
ssl_certificate /etc/letsencrypt/live/posterg.erg.be/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/posterg.erg.be/privkey.pem;
# Logs
access_log /var/log/nginx/posterg_access.log;
error_log /var/log/nginx/posterg_error.log;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Deny access to sensitive files (should already be outside public/)
location ~ /\. {
deny all;
}
location ~ /database/ {
deny all;
}
# Admin area - basic auth
location /admin/ {
auth_basic "Admin Access";
auth_basic_user_file /etc/nginx/.htpasswd;
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.4-fpm.sock;
# IMPORTANT: Set correct SCRIPT_FILENAME
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
# PHP files
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.4-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
# Static files
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Deny access to uploaded files execution
location ~ ^/uploads/.*\.php$ {
deny all;
}
}
```
---
## 🔄 Migration Steps
### Step 1: Local Development
```bash
# 1. Create new structure
mkdir -p public/{admin,assets}
mkdir -p src config var/{cache,logs,tmp}
# 2. Move public files
mv index.php search.php memoire.php public/
mv assets/* public/assets/
mv admin/* public/admin/
# 3. Move private files
mv inc/* config/ # or src/ depending on content
mv lib src/lib
# 4. Update paths in PHP files (see PATHS_UPDATE.md below)
# 5. Test local dev server
just serve
# Opens http://localhost:8000
# Verify that sensitive files return 404:
# http://localhost:8000/database/test.db → 404
# http://localhost:8000/config/ → 404
# http://localhost:8000/src/ → 404
```
### Step 2: Update Nginx Config
```bash
# Edit nginx/posterg.conf
# Change: root /var/www/html;
# To: root /var/www/posterg/public;
# Validate locally
nginx -t -c nginx/posterg.conf
```
### Step 3: Deploy
```bash
# 1. Backup current production
ssh posterg "tar -czf /tmp/posterg-backup-$(date +%Y%m%d).tar.gz /var/www/html"
# 2. Create new directory structure
ssh posterg "mkdir -p /var/www/posterg"
# 3. Deploy application
just deploy
# 4. Deploy nginx config
just deploy-nginx
# 5. On server: activate new config
ssh posterg
sudo bash /tmp/deploy-production.sh
# 6. Reload nginx
sudo systemctl reload nginx
# 7. Test
just server-status
```
---
## 🧪 Testing Checklist
### Local Testing
- [ ] `just serve` starts on port 8000
- [ ] Public site loads: http://localhost:8000
- [ ] Admin loads: http://localhost:8000/admin/
- [ ] Assets load (CSS, images, JS)
- [ ] Database files NOT accessible via browser
- [ ] Config files NOT accessible via browser
- [ ] Tests still pass: `just test`
### Production Testing
- [ ] HTTPS works: https://posterg.erg.be/
- [ ] Admin login works
- [ ] Search functionality works
- [ ] Database connections work
- [ ] File uploads work (if applicable)
- [ ] Logs written to `/var/www/posterg/var/logs/`
- [ ] Sensitive URLs return 404:
- https://posterg.erg.be/database/test.db
- https://posterg.erg.be/config/
- https://posterg.erg.be/src/
- https://posterg.erg.be/vendor/
---
## 📊 Path Changes Summary
| File Type | Current Path | New Path |
|-----------|-------------|----------|
| Public index | `/index.php` | `/public/index.php` |
| Admin panel | `/admin/` | `/public/admin/` |
| Assets | `/assets/` | `/public/assets/` |
| Config | `/inc/` | `/config/` or `/src/` |
| Libraries | `/lib/` | `/src/lib/` |
| Database | `/database/` | `/database/` (stays) |
| Vendor | `/vendor/` | `/vendor/` (stays) |
| Tests | `/tests/` | `/tests/` (stays) |
---
## 🔒 Security Improvements
### Before
- ❌ All files in DocumentRoot
- ❌ Relies on nginx deny rules
- ❌ One misconfiguration = full exposure
- ❌ Database accessible if nginx fails
### After
- ✅ Only `public/` in DocumentRoot
- ✅ Physical separation of public/private
- ✅ Nginx misconfiguration = site down (not exposed)
- ✅ Database physically unreachable via web
---
## 💡 Tips
### Router Script (for clean URLs)
Create `public/router.php` for development:
```php
<?php
// Development router script
if (php_sapi_name() === 'cli-server') {
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
// Serve static files directly
if (file_exists(__DIR__ . $path) && is_file(__DIR__ . $path)) {
return false;
}
// Route everything else to index.php
require __DIR__ . '/index.php';
}
```
### Environment Detection
In your code, detect environment:
```php
<?php
// config/bootstrap.php
define('IS_DEV', php_sapi_name() === 'cli-server');
define('APP_ROOT', dirname(__DIR__)); // /var/www/posterg
define('PUBLIC_ROOT', APP_ROOT . '/public');
```
### Use Relative Requires
```php
// Before (brittle)
require_once '/var/www/html/inc/config.php';
// After (portable)
require_once __DIR__ . '/../config/app.php';
// or
require_once APP_ROOT . '/config/app.php';
```
---
## 🆘 Troubleshooting
### Issue: 404 on all pages after deploy
**Cause:** Nginx DocumentRoot not updated
**Fix:** Check nginx config has `root /var/www/posterg/public;`
### Issue: PHP includes fail
**Cause:** Hardcoded paths or wrong APP_ROOT
**Fix:** Use `__DIR__` or define APP_ROOT constant
### Issue: Database connection fails
**Cause:** Path to database file wrong
**Fix:** Update path from `database/test.db` to `../database/test.db` (from public/)
### Issue: Can't write to cache/logs
**Cause:** Wrong permissions on var/ directory
**Fix:** `sudo chown -R www-data:posterg /var/www/posterg/var && chmod -R 775 /var/www/posterg/var`
---
## Next Steps
1. Create `PATHS_UPDATE.md` - document all PHP file path changes needed
2. Create `public/router.php` - for cleaner dev URLs
3. Update all `require`/`include` statements to use relative paths
4. Test migration on staging/local first
5. Schedule production deployment during low-traffic period

246
DIRECTORY_STRUCTURE.md Normal file
View File

@@ -0,0 +1,246 @@
# Recommended Directory Structure
Based on the **Standard PHP Package Skeleton** (researched by Paul M. Jones from thousands of GitHub projects).
## Directory Layout
```
posterg-website/
├── public/ # DocumentRoot - publicly accessible files
│ ├── index.php # Front controller
│ ├── assets/ # Public assets (CSS, JS, images)
│ │ ├── css/
│ │ ├── js/
│ │ └── images/
│ └── .htaccess # Apache/nginx rules
├── src/ # Application source code (private)
│ ├── Controller/ # Controllers
│ ├── Model/ # Models
│ ├── View/ # Views/templates
│ ├── Service/ # Business logic services
│ ├── Repository/ # Data access layer
│ └── Middleware/ # Middleware components
├── config/ # Configuration files (private)
│ ├── app.php
│ ├── database.php
│ ├── routes.php
│ └── .env.example # Environment variables template
├── database/ # Database-related files
│ ├── migrations/ # Database migrations
│ ├── seeds/ # Database seeders
│ └── schema.sql # Database schema
├── tests/ # Unit and integration tests
│ ├── Unit/
│ ├── Integration/
│ └── bootstrap.php
├── vendor/ # Third-party dependencies (Composer)
│ └── autoload.php
├── bin/ # Executable scripts
│ └── console # CLI commands
├── var/ # Variable/temporary files (private)
│ ├── cache/ # Application cache
│ ├── logs/ # Log files
│ └── tmp/ # Temporary files
├── docs/ # Documentation
│ └── *.md
├── scripts/ # Build/deployment scripts
│ └── deploy.sh
├── resources/ # Non-PHP resources (private)
│ ├── views/ # Template files
│ ├── lang/ # Translations
│ └── emails/ # Email templates
├── lib/ # Internal libraries (if not using src/)
├── .git/ # Git repository
├── .gitignore
├── composer.json # Composer dependencies
├── composer.lock
├── phpunit.xml # PHPUnit configuration
├── README.md # Project documentation
└── LICENSE # License file
```
## Directory Purposes
### **public/** (PUBLIC - DocumentRoot points here)
- **Only directory accessible via web browser**
- Contains: front controller (index.php), assets (CSS/JS/images)
- Web server DocumentRoot should point to this directory
- Security: No sensitive files here
### **src/** (PRIVATE)
- Application source code
- All classes following PSR-4 autoloading
- Organized by responsibility (Controller, Model, Service, etc.)
- Not accessible from the web
### **config/** (PRIVATE)
- Configuration files
- Database credentials, API keys, app settings
- `.env` file for environment-specific configuration
- Never committed sensitive values (use `.env.example`)
### **database/** (PRIVATE)
- Database migrations, seeds, schema definitions
- Version-controlled database structure
### **tests/** (PRIVATE)
- PHPUnit tests
- Organized by test type (Unit, Integration, Functional)
- Mirror the `src/` structure
### **vendor/** (PRIVATE)
- Composer dependencies
- Auto-generated, excluded from version control
- Contains `autoload.php` for autoloading
### **bin/** (PRIVATE)
- Executable scripts and CLI commands
- Make scripts executable: `chmod +x bin/*`
### **var/** (PRIVATE)
- Runtime-generated files
- cache/, logs/, tmp/
- Typically .gitignored (except .gitkeep files)
- Needs write permissions
### **docs/** (PRIVATE)
- Project documentation
- API docs, guides, architecture decisions
### **scripts/** (PRIVATE)
- Build, deployment, maintenance scripts
- Not part of the application runtime
### **resources/** (PRIVATE)
- Non-PHP application resources
- Templates, translations, email layouts
## Migration from Current Structure
Your current structure → Recommended structure:
```
Current → Recommended
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
index.php → public/index.php
memoire.php → public/memoire.php OR src/Controller/
search.php → public/search.php OR src/Controller/
assets/ → public/assets/
inc/ → src/ OR config/
admin/ → src/Admin/ OR public/admin/
database/ → database/ (keep as-is)
tests/ → tests/ (keep as-is)
vendor/ → vendor/ (keep as-is)
lib/ → lib/ OR src/
docs/ → docs/ (keep as-is)
scripts/ → scripts/ (keep as-is)
nginx/ → scripts/nginx/ OR config/nginx/
```
## Security Best Practices
1. **DocumentRoot = public/**
- Configure web server to serve only from `public/`
- All other directories are above DocumentRoot
2. **Sensitive Files**
- Keep `.env`, config files outside `public/`
- Never commit passwords, API keys
- Use `.env.example` for templates
3. **Permissions**
```bash
# Private directories (not writable by web server)
chmod 755 src/ config/ database/ tests/
# Writable by web server
chmod 775 var/cache/ var/logs/ var/tmp/
```
4. **.gitignore**
```
/vendor/
/var/cache/*
/var/logs/*
/var/tmp/*
/.env
composer.lock
```
## Web Server Configuration
### Nginx
```nginx
server {
root /path/to/posterg-website/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
}
}
```
### Apache (.htaccess in public/)
```apache
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [L]
</IfModule>
```
## Composer Configuration
Update `composer.json` to use PSR-4 autoloading:
```json
{
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
}
}
```
## Next Steps
1. Create `public/` directory
2. Move web-accessible files to `public/`
3. Organize classes into `src/` with namespaces
4. Move configuration to `config/`
5. Update web server DocumentRoot
6. Update paths in application code
7. Run `composer dump-autoload`
8. Test the application
## References
- [Standard PHP Package Skeleton](https://github.com/php-pds/skeleton)
- [PSR-4 Autoloading](https://www.php-fig.org/psr/psr-4/)
- [Composer Documentation](https://getcomposer.org/doc/)

381
MIGRATION_CHECKLIST.md Normal file
View File

@@ -0,0 +1,381 @@
# Migration Checklist - Public Directory Structure
Quick reference for migrating from current flat structure to secure public/ structure.
## 📋 Summary of Required Changes
### 1. Dev Server (justfile) ⚡
**Line 51** - Change from:
```just
@php -S 127.0.0.1:8000
```
To:
```just
@php -S 127.0.0.1:8000 -t public/
```
### 2. Deployment (justfile) 📤
**Lines 56-75** - Replace entire `deploy` recipe with two-step deployment:
- Deploy app to `/var/www/posterg/` (not `/var/www/html/`)
- Nginx serves from `/var/www/posterg/public/`
See `DEPLOYMENT_MIGRATION.md` for complete updated recipe.
### 3. Nginx Configuration 🔧
**nginx/posterg.conf line 14** - Change from:
```nginx
root /var/www/html;
```
To:
```nginx
root /var/www/posterg/public;
```
**Also update admin location** (line 58) - Change from:
```nginx
location ^~ /formulaire/ {
```
To:
```nginx
location ^~ /admin/ {
```
(Since you're moving admin/ to public/admin/)
---
## 🚀 Quick Migration (Local Dev)
```bash
# 1. Update justfile serve command
sed -i 's/@php -S 127.0.0.1:8000/@php -S 127.0.0.1:8000 -t public\//' justfile
# 2. Test new dev server
just serve
# Visit http://localhost:8000
# Verify http://localhost:8000/database/test.db returns 404
# 3. If it works, you're ready for production migration
```
---
## 📁 File Movements (Do This After Testing)
```bash
# Create new structure
mkdir -p public/{admin,assets}
mkdir -p src config var/{cache,logs,tmp}
# Move public files
mv index.php search.php memoire.php public/
mv admin/* public/admin/ && rmdir admin
mv assets/* public/assets/ && rmdir assets
# Move private files
mv inc/* config/ && rmdir inc
# OR if inc/ contains classes:
# mv inc/* src/ && rmdir inc
# Keep these as-is
# database/ (already private)
# vendor/ (already private)
# tests/ (already private)
# lib/ (decide if it goes to src/lib or stays)
```
---
## ⚠️ Critical Changes Required
### In nginx/posterg.conf:
1. **Line 14** - DocumentRoot
```nginx
# BEFORE
root /var/www/html;
# AFTER
root /var/www/posterg/public;
```
2. **Line 58-76** - Admin location (already at `/formulaire/`, might want `/admin/`)
```nginx
# BEFORE
location ^~ /formulaire/ {
auth_basic "Admin Access - Post-ERG";
auth_basic_user_file /etc/nginx/.htpasswd-posterg;
# ... rest of config
}
# AFTER (if renaming to /admin/)
location ^~ /admin/ {
auth_basic "Admin Access - Post-ERG";
auth_basic_user_file /etc/nginx/.htpasswd-posterg;
# ... rest of config
}
```
3. **Remove/update deny rules** (lines 48-60) - These become redundant!
```nginx
# BEFORE - needed because everything in DocumentRoot
location ^~ /database/ { deny all; }
location ^~ /shared/ { deny all; }
location ^~ /data/ { deny all; }
# AFTER - can remove! They're already outside public/
# But keep as defense-in-depth:
location ^~ /database/ { deny all; } # Will never match, but safe
```
### In justfile:
**Complete replacement for lines 40-76:**
```just
[group('dev')]
serve:
@echo "🚀 Starting Post-ERG development server"
@echo "========================================"
@echo ""
@echo "📍 Public site: http://localhost:8000"
@echo "📍 Admin panel: http://localhost:8000/admin/"
@echo ""
@echo "🔒 Serving from public/ directory (matches production)"
@if [ -d "vendor/php-live-reload" ]; then \
echo "✨ Live reload enabled"; \
else \
echo "💡 Tip: Run 'just setup' to enable live reload"; \
fi
@echo ""
@echo "Press Ctrl+C to stop"
@echo ""
@php -S 127.0.0.1:8000 -t public/
[group('deploy')]
deploy:
@echo "📤 Deploying Post-ERG complete site"
@echo "===================================="
@echo ""
@echo "Deploying to /var/www/posterg/..."
rsync -vur --progress \
--chown="www-data:posterg" \
--exclude 'vendor' \
--exclude 'tests' \
--exclude 'test.db' \
--exclude '*.md' \
--exclude '.git*' \
--exclude '.jj' \
--exclude '.DS_Store' \
--exclude 'justfile*' \
--exclude 'setup-dev.sh' \
--exclude 'database/backup_*' \
--exclude 'database/fixtures' \
--exclude 'var/cache/*' \
--exclude 'var/logs/*' \
./ posterg:/var/www/posterg/
@echo ""
@echo "Setting up directories and permissions..."
ssh posterg "cd /var/www/posterg && \
mkdir -p var/{cache,logs,tmp} && \
chown -R www-data:posterg var/ database/ && \
chmod -R 775 var/ database/ && \
chmod 660 database/*.db"
@echo ""
@echo "✅ Deployment complete!"
@echo ""
@echo "🔍 Verify:"
@echo " • Public: https://posterg.erg.be/"
@echo " • Admin: https://posterg.erg.be/admin/"
[group('deploy')]
test-deploy:
@echo "⚠️ Deploying test database"
ssh posterg "mkdir -p /var/www/posterg/database"
rsync -vur --progress ./database/test.db posterg:/var/www/posterg/database/
ssh posterg "chown www-data:posterg /var/www/posterg/database/test.db && \
chmod 660 /var/www/posterg/database/test.db"
@echo "✅ Test database deployed"
```
---
## 🧪 Testing Steps
### 1. Test Local Dev Server
```bash
# Start server with new -t public/ flag
just serve
# In another terminal:
curl http://localhost:8000/ # ✅ Should work
curl http://localhost:8000/admin/ # ✅ Should work (after moving)
curl http://localhost:8000/database/test.db # ❌ Should 404
curl http://localhost:8000/config/ # ❌ Should 404
curl http://localhost:8000/vendor/ # ❌ Should 404
```
### 2. Test After File Migration
```bash
# After moving files to public/
just serve
# Test again
curl http://localhost:8000/ # ✅ index.php serves
curl http://localhost:8000/search.php # ✅ works
curl http://localhost:8000/admin/ # ✅ works
curl http://localhost:8000/assets/css/style.css # ✅ works
# Verify old paths don't work
curl http://localhost:8000/../database/test.db # ❌ 404
curl http://localhost:8000/../config/ # ❌ 404
```
### 3. Test Production Deployment
```bash
# After deploying to server
just server-status
# Manual checks
curl -I https://posterg.erg.be/
curl -I https://posterg.erg.be/admin/
curl -I https://posterg.erg.be/database/test.db # Must be 404!
```
---
## 📝 PHP Path Updates Needed
After moving to public/, update PHP includes:
**Before (from root):**
```php
<?php
require_once 'inc/config.php';
require_once 'lib/Database.php';
require_once 'database/test.db';
```
**After (from public/):**
```php
<?php
require_once __DIR__ . '/../config/config.php';
require_once __DIR__ . '/../src/lib/Database.php';
$db = new PDO('sqlite:' . __DIR__ . '/../database/test.db');
```
**Or use a bootstrap:**
```php
<?php
// public/index.php
require_once __DIR__ . '/../config/bootstrap.php';
// config/bootstrap.php
define('APP_ROOT', dirname(__DIR__));
define('PUBLIC_ROOT', APP_ROOT . '/public');
define('DATABASE_PATH', APP_ROOT . '/database/test.db');
require_once APP_ROOT . '/vendor/autoload.php';
```
---
## 🔍 What to Check in Your Code
Run these searches to find hardcoded paths:
```bash
# Find absolute paths
grep -r "/var/www/html" ./*.php
grep -r "/var/www/html" ./admin/*.php
# Find relative paths that need updating
grep -r "require.*inc/" ./*.php
grep -r "require.*lib/" ./*.php
grep -r "require.*database/" ./*.php
# Find database connections
grep -r "\.db" ./*.php
# Find asset references
grep -r "src=\"assets/" ./*.php
grep -r "href=\"assets/" ./*.php
```
---
## ⚡ Quick Start (Minimal Changes First)
If you want to test with minimal changes:
**1. Only change dev server:**
```bash
# Edit justfile line 51
@php -S 127.0.0.1:8000 -t public/
# Create public/ with symlinks (temporary test)
mkdir public
ln -s ../index.php public/index.php
ln -s ../search.php public/search.php
ln -s ../admin public/admin
ln -s ../assets public/assets
# Test
just serve
```
**2. If it works, do full migration**
---
## 🆘 Rollback Plan
If production deployment fails:
```bash
# On server
sudo systemctl stop nginx
# Restore old config
sudo cp /etc/nginx/sites-available/posterg.backup /etc/nginx/sites-available/posterg
sudo nginx -t
sudo systemctl start nginx
# Or restore files
sudo rm -rf /var/www/posterg
sudo mv /var/www/html.backup /var/www/html
```
**Always backup before deploying:**
```bash
# Before migration
ssh posterg "sudo cp -r /var/www/html /var/www/html.backup"
ssh posterg "sudo cp /etc/nginx/sites-available/posterg /etc/nginx/sites-available/posterg.backup"
```
---
## 📊 Benefits After Migration
| Before | After |
|--------|-------|
| ❌ All files in DocumentRoot | ✅ Only public/ in DocumentRoot |
| ❌ Database accessible if nginx misconfigured | ✅ Database physically unreachable |
| ❌ Config files one deny rule away | ✅ Config files outside web root |
| ❌ 20+ deny/exclude rules needed | ✅ Physical separation, minimal rules |
| ❌ Dev server exposes everything | ✅ Dev matches production security |
---
## Next: Check Your Current Paths
Run this to see what paths need updating:
```bash
# Find all require/include statements
find . -name "*.php" -not -path "./vendor/*" -not -path "./tests/*" \
-exec grep -H "require\|include" {} \; > paths-audit.txt
# Review paths-audit.txt and update paths
cat paths-audit.txt
```
See **DEPLOYMENT_MIGRATION.md** for complete implementation details.

246
admin/add.php Normal file
View File

@@ -0,0 +1,246 @@
<?php
// Start session and generate CSRF token
session_start();
if (empty($_SESSION["csrf_token"])) {
$_SESSION["csrf_token"] = bin2hex(random_bytes(32));
}
$pageTitle = "Ajout de TFE";
// Load database helper
require_once __DIR__ . '/../lib/Database.php';
try {
$db = new Database();
$orientations = $db->getAllOrientations();
$apPrograms = $db->getAllAPPrograms();
$finalityTypes = $db->getAllFinalityTypes();
$languages = $db->getAllLanguages();
$formatTypes = $db->getAllFormatTypes();
} catch (Exception $e) {
error_log("Failed to load form data: " . $e->getMessage());
die("Erreur lors du chargement du formulaire. Veuillez réessayer plus tard.");
}
// Get error message and preserved form data from session (if redirected back from error)
$error = isset($_SESSION["form_error"]) ? $_SESSION["form_error"] : null;
$formData = isset($_SESSION["form_data"]) ? $_SESSION["form_data"] : [];
// Clear session data after retrieving
unset($_SESSION["form_error"]);
unset($_SESSION["form_data"]);
// Helper function to get old form value
function old($key, $default = "")
{
global $formData;
return isset($formData[$key])
? htmlspecialchars($formData[$key])
: $default;
}
// Helper function to check if value was previously selected
function wasSelected($key, $value)
{
global $formData;
if (!isset($formData[$key])) {
return false;
}
if (is_array($formData[$key])) {
return in_array($value, $formData[$key]);
}
return $formData[$key] == $value;
}
?>
<? include "inc/head.php"?>
<main>
<?php if ($error): ?>
<div class="error-message" style="background: #fee; border: 2px solid #c00; padding: 1rem; margin-bottom: 1rem; border-radius: 4px; color: #c00;">
<strong>⚠️ Erreur:</strong> <?php echo htmlspecialchars($error); ?>
</div>
<?php endif; ?>
<form action="formulaire.php" method="post" enctype="multipart/form-data">
<!-- CSRF Protection -->
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(
$_SESSION["csrf_token"],
); ?>">
<fieldset>
<legend>Informations de base</legend>
<label for="auteurice">Nom/Prénom/Pseudo *</label>
<input type="text" id="auteurice" name="auteurice" placeholder="Nom de l'auteur·ice" value="<?php echo old(
"auteurice",
); ?>" required>
<label for="mail">Contact (email, site web, insta, ...)</label>
<input type="text" id="mail" name="mail" placeholder="votre.email@example.com ou @instagram" value="<?php echo old(
"mail",
); ?>">
<label for="année">Année diplômante *</label>
<input type="number" id="année" name="année" min="2000" max="<?php echo date(
"Y",
) + 1; ?>" placeholder="<?php echo date(
"Y",
); ?>" value="<?php echo old("année"); ?>" required>
</fieldset>
<fieldset>
<legend>Informations académiques</legend>
<label for="orientation">Orientation principale *</label>
<select id="orientation" name="orientation" required>
<option value="">-- Sélectionner une orientation --</option>
<?php foreach ($orientations as $orientation): ?>
<option value="<?php echo htmlspecialchars(
$orientation["id"],
); ?>" <?php echo wasSelected(
"orientation",
$orientation["id"],
)
? "selected"
: ""; ?>>
<?php echo htmlspecialchars(
$orientation["name"],
); ?>
</option>
<?php endforeach; ?>
</select>
<label for="ap">Atelier Pratique (AP) *</label>
<select id="ap" name="ap" required>
<option value="">-- Sélectionner un AP --</option>
<?php foreach ($apPrograms as $ap): ?>
<option value="<?php echo htmlspecialchars(
$ap["id"],
); ?>" <?php echo wasSelected("ap", $ap["id"])
? "selected"
: ""; ?>>
<?php echo htmlspecialchars($ap["name"]); ?>
<?php if (
$ap["code"]
): ?> (<?php echo htmlspecialchars(
$ap["code"],
); ?>)<?php endif; ?>
</option>
<?php endforeach; ?>
</select>
<label for="finality">Finalité du master *</label>
<select id="finality" name="finality" required>
<option value="">-- Sélectionner une finalité --</option>
<?php foreach ($finalityTypes as $finality): ?>
<option value="<?php echo htmlspecialchars(
$finality["id"],
); ?>" <?php echo wasSelected(
"finality",
$finality["id"],
)
? "selected"
: ""; ?>>
<?php echo htmlspecialchars($finality["name"]); ?>
</option>
<?php endforeach; ?>
</select>
<label for="promoteurice">Promoteur·ice(s)</label>
<input type="text" id="promoteurice" name="promoteurice" placeholder="Nom du/de la promoteur·ice (si plusieurs, séparer par des virgules)" value="<?php echo old(
"promoteurice",
); ?>">
</fieldset>
<fieldset>
<legend>À propos du TFE</legend>
<label for="titre">Titre du mémoire *</label>
<input type="text" id="titre" name="titre" placeholder="Titre de votre TFE" value="<?php echo old(
"titre",
); ?>" required>
<label for="subtitle">Sous-titre (si applicable)</label>
<input type="text" id="subtitle" name="subtitle" placeholder="Sous-titre de votre TFE" value="<?php echo old(
"subtitle",
); ?>">
<label for="synopsis">Synopsis (environ 200 mots) *</label>
<textarea id="synopsis" name="synopsis" rows="8" placeholder="Décrivez votre TFE en quelques paragraphes..." required><?php echo old(
"synopsis",
); ?></textarea>
<label for="problématique">Problématique</label>
<textarea id="problématique" name="problématique" rows="4" placeholder="La problématique principale de votre mémoire..."><?php echo old(
"problématique",
); ?></textarea>
<label>Langue(s) du TFE * (sélection multiple possible)</label>
<ul style="list-style: none;">
<?php foreach ($languages as $language): ?>
<li>
<label class="checkbox-label">
<input type="checkbox" name="languages[]" value="<?php echo htmlspecialchars(
$language["id"],
); ?>" <?php echo wasSelected(
"languages",
$language["id"],
)
? "checked"
: ""; ?>>
<?php echo htmlspecialchars($language["name"]); ?>
</label>
</li>
<?php endforeach; ?>
</ul>
<label>Format(s) (sélection multiple possible)</label>
<ul style="list-style: none;">
<?php foreach ($formatTypes as $format): ?>
<li>
<label class="checkbox-label">
<input type="checkbox" name="formats[]" value="<?php echo htmlspecialchars(
$format["id"],
); ?>" <?php echo wasSelected(
"formats",
$format["id"],
)
? "checked"
: ""; ?>>
<?php echo htmlspecialchars($format["name"]); ?>
</label>
</li>
<?php endforeach; ?>
</ul>
<label for="tag">Mots-clés (max 10, séparés par des virgules)</label>
<input type="text" id="tag" name="tag" placeholder="typographie, photographie, outils libre, post-colonial..." value="<?php echo old(
"tag",
); ?>">
<small>Séparez les mots-clés par des virgules. Maximum 10 mots-clés.</small>
<label for="duration_info">Durée/Taille (si applicable)</label>
<input type="text" id="duration_info" name="duration_info" placeholder="Ex: 68 minutes, 128 pages, 78 pages + 15 minutes" value="<?php echo old(
"duration_info",
); ?>">
<small>Indiquez la durée (en minutes) ou le nombre de pages de votre TFE.</small>
<label for="lien">Lien vers un site web ou ressource en ligne</label>
<input type="url" id="lien" name="lien" placeholder="https://monmemoire.erg.be/..." value="<?php echo old(
"lien",
); ?>">
</fieldset>
<fieldset>
<legend>Fichiers</legend>
<label for="couverture">Importer une image de couverture</label>
<small>Formats acceptés : JPG, PNG. Taille max : 10MB.</small>
<input type="file" id="couverture" name="couverture" accept="image/jpeg,image/png">
<label for="files">Importer le TFE et les fichiers annexes</label>
<small>Formats acceptés : PDF, JPG, PNG, MP4, ZIP. Taille max par fichier : 50MB.</small>
<small>Si vous voulez importer un dossier, créez une archive ZIP.</small>
<input type="file" id="files" name="files[]" multiple accept=".pdf,.jpg,.jpeg,.png,.mp4,.zip">
</fieldset>
<br>
<input type="submit" name="go" value="Soumettre mon TFE">
</form>
</main>
<? include "inc/footer.php"?>

View File

@@ -11,6 +11,8 @@ if (empty($_SESSION['csrf_token'])) {
require_once __DIR__ . '/../lib/Database.php'; require_once __DIR__ . '/../lib/Database.php';
$pageTitle = "Import";
$message = ''; $message = '';
$errors = []; $errors = [];
$importedCount = 0; $importedCount = 0;
@@ -249,7 +251,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
$db->commit(); $db->commit();
$importedCount++; $importedCount++;
$importResults[] = "✓ Ligne $lineNumber: \"$title\" importé (ID: $thesisId)"; $importResults[] = "✓ Ligne $lineNumber: \"$title\" importé (ID: $thesisId)";
} catch (Exception $e) { } catch (Exception $e) {
$db->rollback(); $db->rollback();
$skippedCount++; $skippedCount++;
@@ -261,7 +262,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
fclose($handle); fclose($handle);
$message = "Import terminé : $importedCount TFE importés, $skippedCount ignorés."; $message = "Import terminé : $importedCount TFE importés, $skippedCount ignorés.";
} catch (Exception $e) { } catch (Exception $e) {
$errors[] = $e->getMessage(); $errors[] = $e->getMessage();
error_log("CSV import error: " . $e->getMessage()); error_log("CSV import error: " . $e->getMessage());
@@ -272,24 +272,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32)); $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
} }
?> ?>
<!DOCTYPE html> // <title>Import CSV - Post-ERG</title>
<html lang="fr"> // <header>
<head> // <h1>Import CSV - Post-ERG</h1>
<meta charset="UTF-8"> // <nav>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> // <a href="index.php">← Nouveau TFE</a> |
<title>Import CSV - Post-ERG</title> // <a href="list.php">📋 Liste des TFE</a>
<link rel="stylesheet" href="assets/normalize.css"> // </nav>
<link rel="stylesheet" href="https://raw.githack.com/waldyrious/downstyler/master/downstyler.css" /> // </header>
<link rel="shortcut icon" href="assets/icon.svg" type="image/svg">
</head>
<body>
<header>
<h1>Import CSV - Post-ERG</h1>
<nav>
<a href="index.php">← Nouveau TFE</a> |
<a href="list.php">📋 Liste des TFE</a>
</nav>
</header>
<main> <main>
<h2>Importer des TFE depuis un fichier CSV</h2> <h2>Importer des TFE depuis un fichier CSV</h2>
@@ -359,8 +349,4 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
<p>Voir: <code>../db/Database_TFE_test.csv</code></p> <p>Voir: <code>../db/Database_TFE_test.csv</code></p>
</main> </main>
<footer> <? include "inc/footer.php" ?>
<p>Import CSV - Post-ERG Database</p>
</footer>
</body>
</html>

6
admin/inc/footer.php Normal file
View File

@@ -0,0 +1,6 @@
<footer>
<p>Formulaire fait avec en PHP et <a href="https://watercss.kognise.dev/">Water.css</a>.</p>
</footer>
</body>
</html>

22
admin/inc/head.php Normal file
View File

@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><? echo $pageTitle ?></title>
<link rel="stylesheet" href="/assets/modern-normalize.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/water.css">
<link rel="stylesheet" href="/assets/admin.css">
<link rel="shortcut icon" href="assets/icon.svg" type="image/svg">
</head>
<body>
<header>
<h1><? echo $pageTitle ?></h1>
<nav style="margin-top: 1rem;">
<a href="/admin/list.php" style="font-size: 0.9em;"><button>📋 Liste des TFE</button></a>
<a href="/admin/import.php" style="font-size: 0.9em;"><button>📥 Importer CSV</button></a>
</nav>
</header>

View File

@@ -1,300 +1,285 @@
<?php <?php
// Start session and generate CSRF token // List all theses in the database
session_start(); session_start();
if (empty($_SESSION["csrf_token"])) {
$_SESSION["csrf_token"] = bin2hex(random_bytes(32)); // Generate CSRF token
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
} }
// Load database helper $pageTitle = "Liste des TFE";
$pageMenu
require_once __DIR__ . '/../lib/Database.php'; require_once __DIR__ . '/../lib/Database.php';
try { try {
$db = new Database(); $db = new Database();
$pdo = $db->getPDO();
// Get filter parameters
$searchQuery = isset($_GET['search']) ? trim($_GET['search']) : '';
$yearFilter = isset($_GET['year']) ? intval($_GET['year']) : null;
$orientationFilter = isset($_GET['orientation']) ? intval($_GET['orientation']) : null;
// Build query
$sql = "SELECT
t.id, t.identifier, t.title, t.subtitle, t.year,
o.name as orientation,
ap.name as ap_program,
GROUP_CONCAT(DISTINCT a.name) as authors,
t.submitted_at,
t.is_published
FROM theses t
LEFT JOIN orientations o ON t.orientation_id = o.id
LEFT JOIN ap_programs ap ON t.ap_program_id = ap.id
LEFT JOIN thesis_authors ta ON t.id = ta.thesis_id
LEFT JOIN authors a ON ta.author_id = a.id
WHERE 1=1";
$params = [];
if ($searchQuery) {
$sql .= " AND (t.title LIKE ? OR t.subtitle LIKE ? OR a.name LIKE ?)";
$searchParam = "%$searchQuery%";
$params[] = $searchParam;
$params[] = $searchParam;
$params[] = $searchParam;
}
if ($yearFilter) {
$sql .= " AND t.year = ?";
$params[] = $yearFilter;
}
if ($orientationFilter) {
$sql .= " AND t.orientation_id = ?";
$params[] = $orientationFilter;
}
$sql .= " GROUP BY t.id ORDER BY t.year DESC, t.submitted_at DESC";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$theses = $stmt->fetchAll();
// Get unique years for filter
$yearsStmt = $pdo->query("SELECT DISTINCT year FROM theses ORDER BY year DESC");
$years = $yearsStmt->fetchAll(PDO::FETCH_COLUMN);
// Get orientations for filter
$orientations = $db->getAllOrientations(); $orientations = $db->getAllOrientations();
$apPrograms = $db->getAllAPPrograms();
$finalityTypes = $db->getAllFinalityTypes();
$languages = $db->getAllLanguages();
$formatTypes = $db->getAllFormatTypes();
} catch (Exception $e) { } catch (Exception $e) {
error_log("Failed to load form data: " . $e->getMessage()); error_log("Error loading theses list: " . $e->getMessage());
die("Erreur lors du chargement du formulaire. Veuillez réessayer plus tard."); die("Erreur lors du chargement de la liste.");
}
// Get error message and preserved form data from session (if redirected back from error)
$error = isset($_SESSION["form_error"]) ? $_SESSION["form_error"] : null;
$formData = isset($_SESSION["form_data"]) ? $_SESSION["form_data"] : [];
// Clear session data after retrieving
unset($_SESSION["form_error"]);
unset($_SESSION["form_data"]);
// Helper function to get old form value
function old($key, $default = "")
{
global $formData;
return isset($formData[$key])
? htmlspecialchars($formData[$key])
: $default;
}
// Helper function to check if value was previously selected
function wasSelected($key, $value)
{
global $formData;
if (!isset($formData[$key])) {
return false;
}
if (is_array($formData[$key])) {
return in_array($value, $formData[$key]);
}
return $formData[$key] == $value;
} }
?> ?>
<!DOCTYPE html>
<html lang="en">
<head> <? include "inc/head.php" ?>
<meta charset="UTF-8"> <script>
<meta http-equiv="X-UA-Compatible" content="IE=edge"> function toggleAll(source) {
<meta name="viewport" content="width=device-width, initial-scale=1.0"> const checkboxes = document.querySelectorAll('input[name="selected_theses[]"]');
<title>Formulaire</title> checkboxes.forEach(checkbox => {
<link rel="stylesheet" href="/assets/modern-normalize.css"> checkbox.checked = source.checked;
<link rel="stylesheet" href="https://raw.githack.com/waldyrious/downstyler/master/downstyler.css" /> });
<link rel="shortcut icon" href="assets/icon.svg" type="image/svg"> updateBulkActionsVisibility();
</head> }
<body> function updateBulkActionsVisibility() {
<header> const checkboxes = document.querySelectorAll('input[name="selected_theses[]"]:checked');
<h1>Formulaire Posterg</h1> const bulkActions = document.getElementById('bulk-actions');
<nav style="margin-top: 1rem;"> const selectedCount = document.getElementById('selected-count');
<a href="list.php" style="font-size: 0.9em;">📋 Liste des TFE</a> |
<a href="import.php" style="font-size: 0.9em;">📥 Importer CSV</a> if (checkboxes.length > 0) {
</nav> bulkActions.style.display = 'flex';
</header> selectedCount.textContent = checkboxes.length;
} else {
bulkActions.style.display = 'none';
}
}
function bulkAction(action) {
const checkboxes = document.querySelectorAll('input[name="selected_theses[]"]:checked');
if (checkboxes.length === 0) {
alert('Veuillez sélectionner au moins un TFE.');
return false;
}
const actionText = action === 'publish' ? 'publier' : 'dépublier';
if (!confirm(`Voulez-vous vraiment ${actionText} ${checkboxes.length} TFE(s) ?`)) {
return false;
}
// Set action
document.getElementById('bulk-action-input').value = action;
// Copy selected thesis IDs to hidden form
const bulkCheckboxesContainer = document.getElementById('bulk-checkboxes');
bulkCheckboxesContainer.innerHTML = '';
checkboxes.forEach(checkbox => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'selected_theses[]';
input.value = checkbox.value;
bulkCheckboxesContainer.appendChild(input);
});
// Submit the form
document.getElementById('bulk-form').submit();
return false;
}
document.addEventListener('DOMContentLoaded', function() {
// Add change listeners to all checkboxes
document.querySelectorAll('input[name="selected_theses[]"]').forEach(checkbox => {
checkbox.addEventListener('change', updateBulkActionsVisibility);
});
});
</script>
<main> <main>
<?php if ($error): ?> <?php if (isset($_SESSION['error'])): ?>
<div class="error-message" style="background: #fee; border: 2px solid #c00; padding: 1rem; margin-bottom: 1rem; border-radius: 4px; color: #c00;"> <div style="background: #fee; border: 2px solid #c00; padding: 1rem; margin-bottom: 1rem; border-radius: 4px; color: #c00;">
<strong>⚠️ Erreur:</strong> <?php echo htmlspecialchars($error); ?> <strong>⚠️ Erreur:</strong> <?php echo htmlspecialchars($_SESSION['error']);
unset($_SESSION['error']); ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
<form action="formulaire.php" method="post" enctype="multipart/form-data"> <?php if (isset($_SESSION['success'])): ?>
<!-- CSRF Protection --> <div style="background: #efe; border: 2px solid #0a0; padding: 1rem; margin-bottom: 1rem; border-radius: 4px; color: #0a0;">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars( <strong>✓ <?php echo htmlspecialchars($_SESSION['success']);
$_SESSION["csrf_token"], unset($_SESSION['success']); ?></strong>
); ?>"> </div>
<?php endif; ?>
<h2>Informations de base</h2> <div id="bulk-actions" class="bulk-actions" style="display: none;">
<strong><span id="selected-count">0</span> TFE(s) sélectionné(s)</strong>
<div class="bulk-actions-buttons">
<button type="button" class="btn-bulk-publish" onclick="bulkAction('publish')">Publier la sélection</button>
<button type="button" class="btn-bulk-unpublish" onclick="bulkAction('unpublish')">Dépublier la sélection</button>
</div>
</div>
<fieldset> <form id="bulk-form" method="post" action="publish.php" style="display: none;">
<label for="auteurice">Nom/Prénom/Pseudo *</label> <input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
<input type="text" id="auteurice" name="auteurice" placeholder="Nom de l'auteur·ice" value="<?php echo old( <input type="hidden" id="bulk-action-input" name="action" value="">
"auteurice", <input type="hidden" name="bulk" value="1">
); ?>" required> <div id="bulk-checkboxes"></div>
</fieldset>
<fieldset>
<label for="mail">Contact (email, site web, insta, ...)</label>
<input type="text" id="mail" name="mail" placeholder="votre.email@example.com ou @instagram" value="<?php echo old(
"mail",
); ?>">
</fieldset>
<fieldset>
<label for="année">Année diplômante *</label>
<input type="number" id="année" name="année" min="2000" max="<?php echo date(
"Y",
) + 1; ?>" placeholder="<?php echo date(
"Y",
); ?>" value="<?php echo old("année"); ?>" required>
</fieldset>
<h2>Informations académiques</h2>
<fieldset>
<label for="orientation">Orientation principale *</label>
<select id="orientation" name="orientation" required>
<option value="">-- Sélectionner une orientation --</option>
<?php foreach ($orientations as $orientation): ?>
<option value="<?php echo htmlspecialchars(
$orientation["id"],
); ?>" <?php echo wasSelected(
"orientation",
$orientation["id"],
)
? "selected"
: ""; ?>>
<?php echo htmlspecialchars(
$orientation["name"],
); ?>
</option>
<?php endforeach; ?>
</select>
<fieldset>
<label for="ap">Atelier Pratique (AP) *</label>
<select id="ap" name="ap" required>
<option value="">-- Sélectionner un AP --</option>
<?php foreach ($apPrograms as $ap): ?>
<option value="<?php echo htmlspecialchars(
$ap["id"],
); ?>" <?php echo wasSelected("ap", $ap["id"])
? "selected"
: ""; ?>>
<?php echo htmlspecialchars($ap["name"]); ?>
<?php if (
$ap["code"]
): ?> (<?php echo htmlspecialchars(
$ap["code"],
); ?>)<?php endif; ?>
</option>
<?php endforeach; ?>
</select>
</fieldset>
<fieldset>
<label for="finality">Finalité du master *</label>
<select id="finality" name="finality" required>
<option value="">-- Sélectionner une finalité --</option>
<?php foreach ($finalityTypes as $finality): ?>
<option value="<?php echo htmlspecialchars(
$finality["id"],
); ?>" <?php echo wasSelected(
"finality",
$finality["id"],
)
? "selected"
: ""; ?>>
<?php echo htmlspecialchars($finality["name"]); ?>
</option>
<?php endforeach; ?>
</select>
</fieldset>
<fieldset>
<label for="promoteurice">Promoteur·ice(s)</label>
<input type="text" id="promoteurice" name="promoteurice" placeholder="Nom du/de la promoteur·ice (si plusieurs, séparer par des virgules)" value="<?php echo old(
"promoteurice",
); ?>">
</fieldset>
<h2>À propos du TFE</h2>
<fieldset>
<label for="titre">Titre du mémoire *</label>
<input type="text" id="titre" name="titre" placeholder="Titre de votre TFE" value="<?php echo old(
"titre",
); ?>" required>
</fieldset>
<fieldset>
<label for="subtitle">Sous-titre (si applicable)</label>
<input type="text" id="subtitle" name="subtitle" placeholder="Sous-titre de votre TFE" value="<?php echo old(
"subtitle",
); ?>">
</fieldset>
<fieldset>
<label for="synopsis">Synopsis (environ 200 mots) *</label>
<textarea id="synopsis" name="synopsis" rows="8" placeholder="Décrivez votre TFE en quelques paragraphes..." required><?php echo old(
"synopsis",
); ?></textarea>
</fieldset>
<fieldset>
<label for="problématique">Problématique</label>
<textarea id="problématique" name="problématique" rows="4" placeholder="La problématique principale de votre mémoire..."><?php echo old(
"problématique",
); ?></textarea>
</fieldset>
<fieldset>
<label>Langue(s) du TFE * (sélection multiple possible)</label>
<?php foreach ($languages as $language): ?>
<label class="checkbox-label">
<input type="checkbox" name="languages[]" value="<?php echo htmlspecialchars(
$language["id"],
); ?>" <?php echo wasSelected(
"languages",
$language["id"],
)
? "checked"
: ""; ?>>
<?php echo htmlspecialchars($language["name"]); ?>
</label>
<?php endforeach; ?>
</fieldset>
<fieldset>
<label>Format(s) (sélection multiple possible)</label>
<?php foreach ($formatTypes as $format): ?>
<label class="checkbox-label">
<input type="checkbox" name="formats[]" value="<?php echo htmlspecialchars(
$format["id"],
); ?>" <?php echo wasSelected(
"formats",
$format["id"],
)
? "checked"
: ""; ?>>
<?php echo htmlspecialchars($format["name"]); ?>
</label>
<?php endforeach; ?>
</fieldset>
<fieldset>
<label for="tag">Mots-clés (max 10, séparés par des virgules)</label>
<input type="text" id="tag" name="tag" placeholder="typographie, photographie, outils libre, post-colonial..." value="<?php echo old(
"tag",
); ?>">
<small>Séparez les mots-clés par des virgules. Maximum 10 mots-clés.</small>
</fieldset>
<fieldset>
<label for="duration_info">Durée/Taille (si applicable)</label>
<input type="text" id="duration_info" name="duration_info" placeholder="Ex: 68 minutes, 128 pages, 78 pages + 15 minutes" value="<?php echo old(
"duration_info",
); ?>">
<small>Indiquez la durée (en minutes) ou le nombre de pages de votre TFE.</small>
</fieldset>
<fieldset>
<label for="lien">Lien vers un site web ou ressource en ligne</label>
<input type="url" id="lien" name="lien" placeholder="https://monmemoire.erg.be/..." value="<?php echo old(
"lien",
); ?>">
</fieldset>
<h2>Fichiers</h2>
<fieldset>
<label for="couverture">Importer une image de couverture</label>
<small>Formats acceptés : JPG, PNG. Taille max : 10MB.</small>
<input type="file" id="couverture" name="couverture" accept="image/jpeg,image/png">
</fieldset>
<fieldset>
<label for="files">Importer le TFE et les fichiers annexes</label>
<small>Formats acceptés : PDF, JPG, PNG, MP4, ZIP. Taille max par fichier : 50MB.</small>
<small>Si vous voulez importer un dossier, créez une archive ZIP.</small>
<input type="file" id="files" name="files[]" multiple accept=".pdf,.jpg,.jpeg,.png,.mp4,.zip">
</fieldset>
<br>
<input type="submit" name="go" value="Soumettre mon TFE">
</form> </form>
<div class="stats">
<div class="stat-card">
<div class="stat-number"><?php echo count($theses); ?></div>
<div class="stat-label">TFE total</div>
</div>
<div class="stat-card">
<div class="stat-number"><?php echo count(array_filter($theses, fn($t) => $t['is_published'])); ?></div>
<div class="stat-label">Publiés</div>
</div>
<div class="stat-card">
<div class="stat-number"><?php echo count(array_filter($theses, fn($t) => !$t['is_published'])); ?></div>
<div class="stat-label">En attente</div>
</div>
</div>
<div class="filters">
<form method="get" action="list.php">
<fieldset>
<label for="search">Rechercher</label>
<input type="text" id="search" name="search" placeholder="Titre, auteur..." value="<?php echo htmlspecialchars($searchQuery); ?>">
</fieldset>
<fieldset>
<label for="year">Année</label>
<select id="year" name="year">
<option value="">Toutes</option>
<?php foreach ($years as $year): ?>
<option value="<?php echo $year; ?>" <?php echo $yearFilter == $year ? 'selected' : ''; ?>>
<?php echo $year; ?>
</option>
<?php endforeach; ?>
</select>
</fieldset>
<fieldset>
<label for="orientation">Orientation</label>
<select id="orientation" name="orientation">
<option value="">Toutes</option>
<?php foreach ($orientations as $orientation): ?>
<option value="<?php echo $orientation['id']; ?>" <?php echo $orientationFilter == $orientation['id'] ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($orientation['name']); ?>
</option>
<?php endforeach; ?>
</select>
</fieldset>
<button type="submit">Filtrer</button>
<?php if ($searchQuery || $yearFilter || $orientationFilter): ?>
<a href="list.php">Réinitialiser</a>
<?php endif; ?>
</form>
</div>
<?php if (empty($theses)): ?>
<p>Aucun TFE trouvé.</p>
<?php else: ?>
<table class="thesis-table">
<thead>
<tr>
<th><input type="checkbox" class="select-all-checkbox" onchange="toggleAll(this)" title="Tout sélectionner"></th>
<th>ID</th>
<th>Titre</th>
<th>Auteur(s)</th>
<th>Année</th>
<th>Orientation</th>
<th>AP</th>
<th>Statut</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($theses as $thesis): ?>
<tr>
<td><input type="checkbox" class="select-checkbox" name="selected_theses[]" value="<?php echo $thesis['id']; ?>"></td>
<td><?php echo htmlspecialchars($thesis['identifier'] ?? $thesis['id']); ?></td>
<td>
<div class="thesis-title"><?php echo htmlspecialchars($thesis['title']); ?></div>
<?php if ($thesis['subtitle']): ?>
<div class="thesis-subtitle"><?php echo htmlspecialchars($thesis['subtitle']); ?></div>
<?php endif; ?>
</td>
<td><?php echo htmlspecialchars($thesis['authors'] ?? 'N/A'); ?></td>
<td><?php echo $thesis['year']; ?></td>
<td><?php echo htmlspecialchars($thesis['orientation'] ?? 'N/A'); ?></td>
<td><?php echo htmlspecialchars($thesis['ap_program'] ?? 'N/A'); ?></td>
<td>
<?php if ($thesis['is_published']): ?>
<span class="status-badge status-published">Publié</span>
<?php else: ?>
<span class="status-badge status-pending">En attente</span>
<?php endif; ?>
</td>
<td>
<div class="actions">
<a href="thanks.php?id=<?php echo $thesis['id']; ?>" class="btn btn-view">Voir</a>
<a href="edit.php?id=<?php echo $thesis['id']; ?>" class="btn btn-edit">Éditer</a>
<form method="post" action="publish.php" class="publish-form">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
<input type="hidden" name="thesis_id" value="<?php echo $thesis['id']; ?>">
<?php if ($thesis['is_published']): ?>
<input type="hidden" name="action" value="unpublish">
<button type="submit" class="btn btn-unpublish" onclick="return confirm('Retirer ce TFE de la publication ?');">Dépublier</button>
<?php else: ?>
<input type="hidden" name="action" value="publish">
<button type="submit" class="btn btn-publish">Publier</button>
<?php endif; ?>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</main> </main>
<footer> <? include "inc/footer.php" ?>
<p>Formulaire fait avec ❤ en PHP et <a href="https://github.com/kevquirk/simple.css">SimpleCSS</a>.</p>
</footer>
</body>
</html>

View File

@@ -1,451 +0,0 @@
<?php
// List all theses in the database
session_start();
// Generate CSRF token
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
require_once __DIR__ . '/../lib/Database.php';
try {
$db = new Database();
$pdo = $db->getPDO();
// Get filter parameters
$searchQuery = isset($_GET['search']) ? trim($_GET['search']) : '';
$yearFilter = isset($_GET['year']) ? intval($_GET['year']) : null;
$orientationFilter = isset($_GET['orientation']) ? intval($_GET['orientation']) : null;
// Build query
$sql = "SELECT
t.id, t.identifier, t.title, t.subtitle, t.year,
o.name as orientation,
ap.name as ap_program,
GROUP_CONCAT(DISTINCT a.name) as authors,
t.submitted_at,
t.is_published
FROM theses t
LEFT JOIN orientations o ON t.orientation_id = o.id
LEFT JOIN ap_programs ap ON t.ap_program_id = ap.id
LEFT JOIN thesis_authors ta ON t.id = ta.thesis_id
LEFT JOIN authors a ON ta.author_id = a.id
WHERE 1=1";
$params = [];
if ($searchQuery) {
$sql .= " AND (t.title LIKE ? OR t.subtitle LIKE ? OR a.name LIKE ?)";
$searchParam = "%$searchQuery%";
$params[] = $searchParam;
$params[] = $searchParam;
$params[] = $searchParam;
}
if ($yearFilter) {
$sql .= " AND t.year = ?";
$params[] = $yearFilter;
}
if ($orientationFilter) {
$sql .= " AND t.orientation_id = ?";
$params[] = $orientationFilter;
}
$sql .= " GROUP BY t.id ORDER BY t.year DESC, t.submitted_at DESC";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$theses = $stmt->fetchAll();
// Get unique years for filter
$yearsStmt = $pdo->query("SELECT DISTINCT year FROM theses ORDER BY year DESC");
$years = $yearsStmt->fetchAll(PDO::FETCH_COLUMN);
// Get orientations for filter
$orientations = $db->getAllOrientations();
} catch (Exception $e) {
error_log("Error loading theses list: " . $e->getMessage());
die("Erreur lors du chargement de la liste.");
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Liste des TFE - Post-ERG</title>
<link rel="stylesheet" href="assets/normalize.css">
<link rel="stylesheet" href="https://raw.githack.com/waldyrious/downstyler/master/downstyler.css" />
<link rel="shortcut icon" href="assets/icon.svg" type="image/svg">
<style>
.filters {
background: #f5f5f5;
padding: 1rem;
margin-bottom: 2rem;
border-radius: 4px;
}
.filters form {
display: flex;
gap: 1rem;
flex-wrap: wrap;
align-items: end;
}
.filters fieldset {
margin: 0;
padding: 0;
border: none;
min-width: 200px;
}
.thesis-table {
width: 100%;
border-collapse: collapse;
}
.thesis-table th,
.thesis-table td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid #ddd;
}
.thesis-table th {
background: #f0f0f0;
font-weight: bold;
}
.thesis-table tr:hover {
background: #f9f9f9;
}
.thesis-title {
font-weight: bold;
}
.thesis-subtitle {
font-style: italic;
color: #666;
font-size: 0.9em;
}
.status-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 3px;
font-size: 0.85em;
}
.status-pending {
background: #ffd700;
color: #000;
}
.status-published {
background: #90ee90;
color: #000;
}
.actions {
display: flex;
gap: 0.5rem;
}
.btn {
padding: 0.35rem 0.75rem;
border-radius: 3px;
text-decoration: none;
font-size: 0.9em;
display: inline-block;
}
.btn-view {
background: #4a90e2;
color: white;
}
.btn-edit {
background: #f39c12;
color: white;
}
.btn-publish {
background: #27ae60;
color: white;
border: none;
cursor: pointer;
}
.btn-unpublish {
background: #95a5a6;
color: white;
border: none;
cursor: pointer;
}
.publish-form {
display: inline;
margin: 0;
}
.stats {
display: flex;
gap: 2rem;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.stat-card {
background: #f5f5f5;
padding: 1rem;
border-radius: 4px;
min-width: 150px;
}
.stat-number {
font-size: 2em;
font-weight: bold;
color: #4a90e2;
}
.stat-label {
color: #666;
font-size: 0.9em;
}
.bulk-actions {
background: #f5f5f5;
padding: 1rem;
margin-bottom: 1rem;
border-radius: 4px;
display: flex;
gap: 1rem;
align-items: center;
}
.bulk-actions-buttons {
display: flex;
gap: 0.5rem;
}
.btn-bulk-publish {
background: #27ae60;
color: white;
border: none;
cursor: pointer;
padding: 0.5rem 1rem;
border-radius: 3px;
}
.btn-bulk-unpublish {
background: #95a5a6;
color: white;
border: none;
cursor: pointer;
padding: 0.5rem 1rem;
border-radius: 3px;
}
.select-checkbox {
cursor: pointer;
}
.select-all-checkbox {
cursor: pointer;
}
</style>
<script>
function toggleAll(source) {
const checkboxes = document.querySelectorAll('input[name="selected_theses[]"]');
checkboxes.forEach(checkbox => {
checkbox.checked = source.checked;
});
updateBulkActionsVisibility();
}
function updateBulkActionsVisibility() {
const checkboxes = document.querySelectorAll('input[name="selected_theses[]"]:checked');
const bulkActions = document.getElementById('bulk-actions');
const selectedCount = document.getElementById('selected-count');
if (checkboxes.length > 0) {
bulkActions.style.display = 'flex';
selectedCount.textContent = checkboxes.length;
} else {
bulkActions.style.display = 'none';
}
}
function bulkAction(action) {
const checkboxes = document.querySelectorAll('input[name="selected_theses[]"]:checked');
if (checkboxes.length === 0) {
alert('Veuillez sélectionner au moins un TFE.');
return false;
}
const actionText = action === 'publish' ? 'publier' : 'dépublier';
if (!confirm(`Voulez-vous vraiment ${actionText} ${checkboxes.length} TFE(s) ?`)) {
return false;
}
// Set action
document.getElementById('bulk-action-input').value = action;
// Copy selected thesis IDs to hidden form
const bulkCheckboxesContainer = document.getElementById('bulk-checkboxes');
bulkCheckboxesContainer.innerHTML = '';
checkboxes.forEach(checkbox => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'selected_theses[]';
input.value = checkbox.value;
bulkCheckboxesContainer.appendChild(input);
});
// Submit the form
document.getElementById('bulk-form').submit();
return false;
}
document.addEventListener('DOMContentLoaded', function() {
// Add change listeners to all checkboxes
document.querySelectorAll('input[name="selected_theses[]"]').forEach(checkbox => {
checkbox.addEventListener('change', updateBulkActionsVisibility);
});
});
</script>
</head>
<body>
<header>
<h1>Liste des TFE</h1>
<nav>
<a href="index.php">← Nouveau TFE</a> |
<a href="import.php">📥 Importer CSV</a>
</nav>
</header>
<main>
<?php if (isset($_SESSION['error'])): ?>
<div style="background: #fee; border: 2px solid #c00; padding: 1rem; margin-bottom: 1rem; border-radius: 4px; color: #c00;">
<strong>⚠️ Erreur:</strong> <?php echo htmlspecialchars($_SESSION['error']); unset($_SESSION['error']); ?>
</div>
<?php endif; ?>
<?php if (isset($_SESSION['success'])): ?>
<div style="background: #efe; border: 2px solid #0a0; padding: 1rem; margin-bottom: 1rem; border-radius: 4px; color: #0a0;">
<strong>✓ <?php echo htmlspecialchars($_SESSION['success']); unset($_SESSION['success']); ?></strong>
</div>
<?php endif; ?>
<div id="bulk-actions" class="bulk-actions" style="display: none;">
<strong><span id="selected-count">0</span> TFE(s) sélectionné(s)</strong>
<div class="bulk-actions-buttons">
<button type="button" class="btn-bulk-publish" onclick="bulkAction('publish')">Publier la sélection</button>
<button type="button" class="btn-bulk-unpublish" onclick="bulkAction('unpublish')">Dépublier la sélection</button>
</div>
</div>
<form id="bulk-form" method="post" action="publish.php" style="display: none;">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
<input type="hidden" id="bulk-action-input" name="action" value="">
<input type="hidden" name="bulk" value="1">
<div id="bulk-checkboxes"></div>
</form>
<div class="stats">
<div class="stat-card">
<div class="stat-number"><?php echo count($theses); ?></div>
<div class="stat-label">TFE total</div>
</div>
<div class="stat-card">
<div class="stat-number"><?php echo count(array_filter($theses, fn($t) => $t['is_published'])); ?></div>
<div class="stat-label">Publiés</div>
</div>
<div class="stat-card">
<div class="stat-number"><?php echo count(array_filter($theses, fn($t) => !$t['is_published'])); ?></div>
<div class="stat-label">En attente</div>
</div>
</div>
<div class="filters">
<form method="get" action="list.php">
<fieldset>
<label for="search">Rechercher</label>
<input type="text" id="search" name="search" placeholder="Titre, auteur..." value="<?php echo htmlspecialchars($searchQuery); ?>">
</fieldset>
<fieldset>
<label for="year">Année</label>
<select id="year" name="year">
<option value="">Toutes</option>
<?php foreach ($years as $year): ?>
<option value="<?php echo $year; ?>" <?php echo $yearFilter == $year ? 'selected' : ''; ?>>
<?php echo $year; ?>
</option>
<?php endforeach; ?>
</select>
</fieldset>
<fieldset>
<label for="orientation">Orientation</label>
<select id="orientation" name="orientation">
<option value="">Toutes</option>
<?php foreach ($orientations as $orientation): ?>
<option value="<?php echo $orientation['id']; ?>" <?php echo $orientationFilter == $orientation['id'] ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($orientation['name']); ?>
</option>
<?php endforeach; ?>
</select>
</fieldset>
<button type="submit">Filtrer</button>
<?php if ($searchQuery || $yearFilter || $orientationFilter): ?>
<a href="list.php">Réinitialiser</a>
<?php endif; ?>
</form>
</div>
<?php if (empty($theses)): ?>
<p>Aucun TFE trouvé.</p>
<?php else: ?>
<table class="thesis-table">
<thead>
<tr>
<th><input type="checkbox" class="select-all-checkbox" onchange="toggleAll(this)" title="Tout sélectionner"></th>
<th>ID</th>
<th>Titre</th>
<th>Auteur(s)</th>
<th>Année</th>
<th>Orientation</th>
<th>AP</th>
<th>Statut</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($theses as $thesis): ?>
<tr>
<td><input type="checkbox" class="select-checkbox" name="selected_theses[]" value="<?php echo $thesis['id']; ?>"></td>
<td><?php echo htmlspecialchars($thesis['identifier'] ?? $thesis['id']); ?></td>
<td>
<div class="thesis-title"><?php echo htmlspecialchars($thesis['title']); ?></div>
<?php if ($thesis['subtitle']): ?>
<div class="thesis-subtitle"><?php echo htmlspecialchars($thesis['subtitle']); ?></div>
<?php endif; ?>
</td>
<td><?php echo htmlspecialchars($thesis['authors'] ?? 'N/A'); ?></td>
<td><?php echo $thesis['year']; ?></td>
<td><?php echo htmlspecialchars($thesis['orientation'] ?? 'N/A'); ?></td>
<td><?php echo htmlspecialchars($thesis['ap_program'] ?? 'N/A'); ?></td>
<td>
<?php if ($thesis['is_published']): ?>
<span class="status-badge status-published">Publié</span>
<?php else: ?>
<span class="status-badge status-pending">En attente</span>
<?php endif; ?>
</td>
<td>
<div class="actions">
<a href="thanks.php?id=<?php echo $thesis['id']; ?>" class="btn btn-view">Voir</a>
<a href="edit.php?id=<?php echo $thesis['id']; ?>" class="btn btn-edit">Éditer</a>
<form method="post" action="publish.php" class="publish-form">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
<input type="hidden" name="thesis_id" value="<?php echo $thesis['id']; ?>">
<?php if ($thesis['is_published']): ?>
<input type="hidden" name="action" value="unpublish">
<button type="submit" class="btn btn-unpublish" onclick="return confirm('Retirer ce TFE de la publication ?');">Dépublier</button>
<?php else: ?>
<input type="hidden" name="action" value="publish">
<button type="submit" class="btn btn-publish">Publier</button>
<?php endif; ?>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</main>
<footer>
<p>Post-ERG - <?php echo count($theses); ?> TFE dans la base de données</p>
</footer>
</body>
</html>

View File

@@ -0,0 +1,180 @@
main {
margin: 1.2rem 0;
}
.filters {
background: #f5f5f5;
padding: 1rem;
margin-bottom: 2rem;
border-radius: 4px;
}
.filters form {
display: flex;
gap: 1rem;
flex-wrap: wrap;
align-items: end;
}
.filters fieldset {
margin: 0;
padding: 0;
border: none;
min-width: 200px;
}
.thesis-table {
width: 100%;
border-collapse: collapse;
}
.thesis-table th,
.thesis-table td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid #ddd;
}
.thesis-table th {
background: #f0f0f0;
font-weight: bold;
}
.thesis-table tr:hover {
background: #f9f9f9;
}
.thesis-title {
font-weight: bold;
}
.thesis-subtitle {
font-style: italic;
color: #666;
font-size: 0.9em;
}
.status-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 3px;
font-size: 0.85em;
}
.status-pending {
background: #ffd700;
color: #000;
}
.status-published {
background: #90ee90;
color: #000;
}
.actions {
display: flex;
gap: 0.5rem;
}
.btn {
padding: 0.35rem 0.75rem;
border-radius: 3px;
text-decoration: none;
font-size: 0.9em;
display: inline-block;
}
.btn-view {
background: #4a90e2;
color: white;
}
.btn-edit {
background: #f39c12;
color: white;
}
.btn-publish {
background: #27ae60;
color: white;
border: none;
cursor: pointer;
}
.btn-unpublish {
background: #95a5a6;
color: white;
border: none;
cursor: pointer;
}
.publish-form {
display: inline;
margin: 0;
}
.stats {
display: flex;
gap: 2rem;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.stat-card {
background: #f5f5f5;
padding: 1rem;
border-radius: 4px;
min-width: 150px;
}
.stat-number {
font-size: 2em;
font-weight: bold;
color: #4a90e2;
}
.stat-label {
color: #666;
font-size: 0.9em;
}
.bulk-actions {
background: #f5f5f5;
padding: 1rem;
margin-bottom: 1rem;
border-radius: 4px;
display: flex;
gap: 1rem;
align-items: center;
}
.bulk-actions-buttons {
display: flex;
gap: 0.5rem;
}
.btn-bulk-publish {
background: #27ae60;
color: white;
border: none;
cursor: pointer;
padding: 0.5rem 1rem;
border-radius: 3px;
}
.btn-bulk-unpublish {
background: #95a5a6;
color: white;
border: none;
cursor: pointer;
padding: 0.5rem 1rem;
border-radius: 3px;
}
.select-checkbox {
cursor: pointer;
}
.select-all-checkbox {
cursor: pointer;
}

View File

@@ -1,54 +1,39 @@
@font-face { @font-face {
font-family: police1; font-family: police1;
src: url("./Combinedd.otf"); src: url("./fonts/Combinedd.otf");
} }
/* Dark theme */ /* Dark theme */
/* UTILE POUR FORCER UN MODE LIGHT */ /* UTILE POUR FORCER UN MODE LIGHT */
@media (prefers-color-scheme: dark) { /* @media (prefers-color-scheme: dark) { */
:root, /* :root, */
::backdrop { /* ::backdrop { */
--bg: #fff; /* --bg: #fff; */
--accent-bg: #f5f7ff; /* --accent-bg: #f5f7ff; */
--text: #212121; /* --text: #212121; */
--text-light: #585858; /* --text-light: #585858; */
--border: #898EA4; /* --border: #898ea4; */
--accent: #0d47a1; /* --accent: #0d47a1; */
--code: #d81b60; /* --code: #d81b60; */
--preformatted: #444; /* --preformatted: #444; */
--marked: #ffdd33; /* --marked: #ffdd33; */
--disabled: #efefef; /* --disabled: #efefef; */
} /* } */
} /* } */
body { body {
background-color: white; background-color: white;
} margin: 0;
/* ENTÊTE */
header {
font-family: 'police1';
background: linear-gradient(280deg, rgba(77, 168, 112, 1) 0%, rgba(193, 4, 252, 1) 85%);
text-decoration: none;
outline: none;
font-size: 2rem;
}
body > header h1 {
color: white;
margin: 3rem auto auto auto;
} }
/* FORMULAIRE */ /* FORMULAIRE */
form label { form label {
font-family: police1; font-family: police1;
font-size: 1rem; font-size: 1rem;
} }
form input, form input,
select, textarea { select,
textarea {
border-color: #c104fc; border-color: #c104fc;
overflow: visible; overflow: visible;
outline: none; outline: none;
@@ -85,17 +70,27 @@ input {
input:active { input:active {
border-color: rgba(77, 168, 112, 1); border-color: rgba(77, 168, 112, 1);
} }
button, [role="button"], input[type="submit"], input[type="reset"], input[type="button"], label[type="button"] { button,
background-color: rgb(193, 4, 252); [role="button"],
input[type="submit"],
input[type="reset"],
input[type="button"],
label[type="button"] {
background-color: white;
margin-top: 2rem; margin-top: 2rem;
font-size: 16px;
border-radius: 10px;
padding: 2ch;
margin: 1rem;
a {
color: black;
text-decoration: none;
}
} }
button {}
/* For Google Chrome, Safari, and newer versions of Opera */ /* For Google Chrome, Safari, and newer versions of Opera */
::placeholder { ::placeholder {
@@ -106,4 +101,5 @@ input {
/* For Mozilla Firefox */ /* For Mozilla Firefox */
::-moz-placeholder { ::-moz-placeholder {
/* color: rgb(213, 73, 255); */ /* color: rgb(213, 73, 255); */
font-size: 0.8rem;} font-size: 0.8rem;
}

View File

@@ -0,0 +1,101 @@
body {
margin: 0;
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
header {
height: 20vh;
}
main {
height: 60vh;
}
footer {
height: 20vh;
}
body {
display: flex;
flex-direction: column;
}
header, main, footer {
padding: 1rem;
margin: 0;
border-radius: 40px;
}
header {
font-family: "police1";
background: #9557b5ff;
color: white;
font-size: 2rem;
display: flex;
gap: 6%;
padding: 1rem 4rem;
.title {
color: white;
}
}
header section p:not(:first-child) {
font-size: 14px;
}
header .title, header section, header nav {
text-decoration: none;
outline: none;
font-size: 18px;
text-decoration: none;
line-height: 2.5rem;
}
main {
height: 60vh;
display: grid;
grid-template-rows: repeat(2, minmax(0, 1fr));
grid-auto-flow: column;
/* critical: force column width so new columns form */
grid-auto-columns: 260px;
gap: 1rem;
padding: 1rem;
box-sizing: border-box;
overflow-x: auto;
overflow-y: hidden;
}
.card {
background: #eee;
border-radius: 10px;
padding: 1rem;
}
main {
background: #3c856bff;
}
footer {
background: #222222ff;
}
/* .card { */
/* width: 20%; */
/* border: 1px solid white; */
/* color: white; */
/* margin: 1ch; */
/* padding: 2ch; */
/* } */
main {
scroll-snap-type: x mandatory;
}
.card {
scroll-snap-align: start;
}
.item {
width: 50%;
}

View File

@@ -1,9 +1,12 @@
<!-- footer.php --> <!-- footer.php -->
<footer class="footer"> <footer>
<div class="content has-text-centered"> <div>
<p>Site fait avec en PHP, HTML, CSS fait mains.</p> <button>Année</button>
</div>
<div>
<button>Cursus</button>
</div> </div>
</footer> </footer>
</body> </body>
</html>
</html>

View File

@@ -8,8 +8,9 @@
<meta name="description" content=""> <meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Posterg</title> <title>Posterg</title>
<link rel="stylesheet" href="assets/normalize.css"> <link rel="stylesheet" href="assets/modern-normalize.css">
<link rel="stylesheet" href="assets/posterg.css?v=2"> <link rel="stylesheet" href="assets/common.css">
<link rel="stylesheet" href="assets/main.css">
<?php if (getenv('PHP_ENV') === 'development' || php_sapi_name() === 'cli-server'): ?> <?php if (getenv('PHP_ENV') === 'development' || php_sapi_name() === 'cli-server'): ?>
<!-- Live reload for development --> <!-- Live reload for development -->
<script src="/vendor/php-live-reload/php-live-reload/live-reload.js"></script> <script src="/vendor/php-live-reload/php-live-reload/live-reload.js"></script>

View File

@@ -3,6 +3,8 @@ ini_set("display_errors", 0);
ini_set("log_errors", 1); ini_set("log_errors", 1);
ini_set("error_log", "error.log"); ini_set("error_log", "error.log");
$pageTitle = "Liste des TFE";
require_once __DIR__ . "/lib/Database.php"; require_once __DIR__ . "/lib/Database.php";
$page = isset($_GET["page"]) ? intval($_GET["page"]) : 1; $page = isset($_GET["page"]) ? intval($_GET["page"]) : 1;
@@ -20,15 +22,10 @@ try {
$totalPages = 0; $totalPages = 0;
} }
include "inc/header.php"; include "inc/header.php"; ?>
?>
<main>
<section class="section">
<div class="container">
<div class="columns is-multiline">
<main>
<?php foreach ($itemsToLoad as $item): ?> <?php foreach ($itemsToLoad as $item): ?>
<div class="column is-one-fifth">
<a href="memoire.php?id=<?= $item["id"] ?>" class="card-link"> <a href="memoire.php?id=<?= $item["id"] ?>" class="card-link">
<div class="card"> <div class="card">
<?php <?php
@@ -84,12 +81,6 @@ include "inc/header.php";
</div> </div>
</a> </a>
</div>
<?php endforeach; ?> <?php endforeach; ?>
</div>
</div>
</section>
</main> </main>
<?php include "inc/footer.php"; <?php include "inc/footer.php"; ?>
?>

View File

@@ -65,6 +65,7 @@ deploy:
@echo "" @echo ""
@echo "Deploying public site..." @echo "Deploying public site..."
rsync -vur --progress \ rsync -vur --progress \
--chown="root:posterg" \
--exclude 'vendor' \ --exclude 'vendor' \
--exclude 'tests' \ --exclude 'tests' \
--exclude 'test.db' \ --exclude 'test.db' \
@@ -72,8 +73,8 @@ deploy:
--exclude 'cache' \ --exclude 'cache' \
--exclude '*.md' \ --exclude '*.md' \
--exclude '.git*' \ --exclude '.git*' \
--exclude '.jj' \
--exclude '.DS_Store' \ --exclude '.DS_Store' \
--exclude 'admin' \
--exclude 'database' \ --exclude 'database' \
--exclude 'nginx' \ --exclude 'nginx' \
--exclude 'docs' \ --exclude 'docs' \
@@ -82,17 +83,6 @@ deploy:
--exclude 'setup-dev.sh' \ --exclude 'setup-dev.sh' \
./ posterg:/var/www/html/ ./ posterg:/var/www/html/
@echo "" @echo ""
@echo "Deploying admin panel..."
rsync -vur --progress \
--exclude 'test.db' \
--exclude '*.db' \
--exclude 'cache' \
--exclude '*.md' \
./admin/ posterg:/var/www/html/admin/
@echo ""
@echo "Deploying shared libraries..."
rsync -vur --progress --exclude 'test.db' ./lib/ posterg:/var/www/html/lib/
@echo ""
@echo "Fixing permissions..." @echo "Fixing permissions..."
ssh posterg "chgrp -R posterg /var/www/html/inc /var/www/html/lib && \ ssh posterg "chgrp -R posterg /var/www/html/inc /var/www/html/lib && \
chmod 755 /var/www/html/inc && chmod 644 /var/www/html/inc/* && \ chmod 755 /var/www/html/inc && chmod 644 /var/www/html/inc/* && \
@@ -105,6 +95,15 @@ deploy:
@echo " • Public: https://posterg.erg.be/" @echo " • Public: https://posterg.erg.be/"
@echo " • Admin: https://posterg.erg.be/admin/" @echo " • Admin: https://posterg.erg.be/admin/"
[group('deploy')]
test-deploy:
@echo "⚠️ Deploying test database (will overwrite remote test.db)"
@echo "Creating database directory if needed..."
ssh posterg "mkdir -p /var/www/html/database"
rsync -vur --progress ./database/test.db posterg:/var/www/html/database/test.db
@echo "Setting correct permissions..."
ssh posterg "chgrp posterg /var/www/html/database /var/www/html/database/test.db && chmod 775 /var/www/html/database && chmod 660 /var/www/html/database/test.db"
@echo "✅ Test database deployed and configured"
# ============================================================================ # ============================================================================
# Testing # Testing
# ============================================================================ # ============================================================================

View File

@@ -1 +1 @@
[1770317579] [1770319036]

View File

@@ -1 +1 @@
[1770318922,1770318923,1770318924,1770318926,1770318930] [1770322829]

View File

@@ -32,13 +32,8 @@ if (isset($_GET['id'])) {
// Include the header template // Include the header template
include 'inc/header.php'; ?> include 'inc/header.php'; ?>
<main>
<section class="section"> <div class="item">
<div class="container">
<div class="columns is-variable is-1-mobile is-0-tablet is-3-desktop is-8-widescreen is-2-fullhd">
<!-- INFO CARD -->
<div class="column is-one-third">
<div class="card">
<div class="card-content"> <div class="card-content">
<!-- Display the title and author from the database --> <!-- Display the title and author from the database -->
<h1 class="title"> <h1 class="title">
@@ -155,11 +150,7 @@ include 'inc/header.php'; ?>
<p class="notification is-warning">Aucun fichier disponible pour ce mémoire.</p> <p class="notification is-warning">Aucun fichier disponible pour ce mémoire.</p>
<?php endif; ?> <?php endif; ?>
</div> </div>
</main>
</div>
</div>
</div>
</section>
<!-- Include the footer template --> <!-- Include the footer template -->
<?php include 'inc/footer.php'; ?> <?php include 'inc/footer.php'; ?>