fix(production): fix multiple remote server errors from nginx logs

- Fix 413 Request Entity Too Large: bump client_max_body_size to 256M,
  PHP post_max_size/upload_max_filesize to 256M, fastcgi timeouts to 300s
- Fix missing v_smtp_active view: add IF NOT EXISTS to all CREATE VIEW
  statements in schema.sql for idempotent migrates
- Fix bars.svg 404: create animated SVG spinner in app/public/assets/img/
- Fix nginx rate limiting: increase admin zone from 60r/m (1 r/s) to
  300r/m (5 r/s) with burst=30 to handle ~11 concurrent HTMX fragment
  GETs on contenus.php page load
- Add deploy-nginx recipe to justfile for uploading nginx config to server
- Database readonly issue mitigated by existing --chown + deploy-server.sh
  permissions fix
- Add comprehensive PHP/JS debugging logs for settings checkboxes:
  per-field raw POST values in error_log, console.log on htmx:beforeSend,
  htmx:sendError, htmx:afterRequest, toast lifecycle
- Fix toast auto-remove script: use getElementById with unique ID instead
  of querySelector which could remove wrong toast on rapid clicks
This commit is contained in:
Pontoporeia
2026-05-11 03:18:03 +02:00
parent 43064ccbd7
commit be50ac5eb0
9 changed files with 119 additions and 30 deletions

View File

@@ -5,6 +5,14 @@
- [x] Add `hx-target` response divs to the three fieldsets in contenus.php - [x] Add `hx-target` response divs to the three fieldsets in contenus.php
- [x] Update settings.php to return HTML toast on HTMX requests - [x] Update settings.php to return HTML toast on HTMX requests
## Production Error Fixes (2026-05-11 remote logs)
- [x] **413 Request Entity Too Large** — bumped `client_max_body_size` to 256M, PHP post/upload to 256M, timeouts to 300s
- [x] **Missing `v_smtp_active` view** on server — made all `CREATE VIEW` statements idempotent with `IF NOT EXISTS` in schema.sql
- [x] **`bars.svg` 404** — created `app/public/assets/img/bars.svg` (animated SVG spinner)
- [x] **Nginx rate limiting too aggressive** — increased admin zone to 300r/m, burst=30 to handle ~11 concurrent HTMX fragment requests on contenus.php page load
- [ ] **Database readonly** — intermittent permission issue after deploy (added deploy-nginx recipe; permissions should be fixed by --chown + deploy-server.sh)
## SQLite Backup & Data Integrity (docs/backup-plan.md) ## SQLite Backup & Data Integrity (docs/backup-plan.md)
### Phase 1 — WAL Mode ### Phase 1 — WAL Mode

View File

@@ -6,6 +6,7 @@ AdminAuth::requireLogin();
if (!isset($_POST['csrf_token'], $_SESSION['csrf_token']) if (!isset($_POST['csrf_token'], $_SESSION['csrf_token'])
|| !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) { || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
error_log('[settings.php] CSRF FAIL | session_token=' . ($_SESSION['csrf_token'] ?? 'none') . ' | post_token=' . ($_POST['csrf_token'] ?? 'none'));
App::flash('error', "Erreur de sécurité : token invalide."); App::flash('error', "Erreur de sécurité : token invalide.");
header('Location: /admin/parametres.php'); header('Location: /admin/parametres.php');
exit; exit;
@@ -27,29 +28,41 @@ error_log('[settings.php] PROCESS | section=' . $section . ' | post_keys=' . imp
* The fragment auto-dismisses after 3 seconds via a script at the end. * The fragment auto-dismisses after 3 seconds via a script at the end.
*/ */
function hxToastSuccess(string $message): never { function hxToastSuccess(string $message): never {
$id = 'toast-' . bin2hex(random_bytes(4));
http_response_code(200); http_response_code(200);
echo '<div class="toast toast--success" role="status" data-toast-autoremove>' . echo '<div class="toast toast--success" role="status" id="' . $id . '">' .
'<span class="toast__icon" aria-hidden="true">✓</span> ' . '<span class="toast__icon" aria-hidden="true">✓</span> ' .
htmlspecialchars($message) . '</div>' . htmlspecialchars($message) . '</div>' .
'<script>setTimeout(function(){var t=document.querySelector("[data-toast-autoremove]");if(t)t.remove()},3000)</script>'; '<script>' .
'(function(){console.log("[settings-toast] success: ' . htmlspecialchars(addslashes($message), ENT_QUOTES) . '");' .
'var el=document.getElementById("' . $id . '");' .
'if(el){setTimeout(function(){el.remove();console.log("[settings-toast] removed ' . $id . '")},3000)}' .
'})()</script>';
exit; exit;
} }
function hxToastError(string $message): never { function hxToastError(string $message): never {
$id = 'toast-' . bin2hex(random_bytes(4));
http_response_code(200); http_response_code(200);
echo '<div class="toast toast--error" role="alert" data-toast-autoremove>' . echo '<div class="toast toast--error" role="alert" id="' . $id . '">' .
'<span class="toast__icon" aria-hidden="true">⚠</span> ' . '<span class="toast__icon" aria-hidden="true">⚠</span> ' .
htmlspecialchars($message) . '</div>' . htmlspecialchars($message) . '</div>' .
'<script>setTimeout(function(){var t=document.querySelector("[data-toast-autoremove]");if(t)t.remove()},3000)</script>'; '<script>' .
'(function(){console.warn("[settings-toast] error: ' . htmlspecialchars(addslashes($message), ENT_QUOTES) . '");' .
'var el=document.getElementById("' . $id . '");' .
'if(el){setTimeout(function(){el.remove();console.log("[settings-toast] removed ' . $id . '")},3000)}' .
'})()</script>';
exit; exit;
} }
if ($section === 'formulaire_restrictions') { if ($section === 'formulaire_restrictions') {
// HTMX may not send unchecked checkboxes even with hidden 0-value inputs; // HTMX may not send unchecked checkboxes even with hidden 0-value inputs;
// missing key means unchecked → treat as '0'. // missing key means unchecked → treat as '0'.
$newValues = ['restricted_files_enabled' => empty($_POST['restricted_files_enabled']) ? '0' : '1']; $rawPost = $_POST['restricted_files_enabled'] ?? '(missing)';
$db->setSetting('restricted_files_enabled', $newValues['restricted_files_enabled']); $newValue = empty($_POST['restricted_files_enabled']) ? '0' : '1';
$logger->logFormSettingsUpdate($newValues); error_log('[settings.php] SAVE formulaire_restrictions | restricted_files_enabled raw=' . var_export($rawPost, true) . ' | resolved=' . $newValue);
$db->setSetting('restricted_files_enabled', $newValue);
$logger->logFormSettingsUpdate(['restricted_files_enabled' => $newValue]);
if ($isHxRequest) { if ($isHxRequest) {
hxToastSuccess('Restrictions d\'accès aux fichiers mises à jour.'); hxToastSuccess('Restrictions d\'accès aux fichiers mises à jour.');
} else { } else {
@@ -63,7 +76,9 @@ if ($section === 'formulaire_restrictions') {
]; ];
$newValues = []; $newValues = [];
foreach ($allowed as $key) { foreach ($allowed as $key) {
$raw = $_POST[$key] ?? '(missing)';
$value = empty($_POST[$key]) ? '0' : '1'; $value = empty($_POST[$key]) ? '0' : '1';
error_log('[settings.php] SAVE formulaire_acces | ' . $key . ' raw=' . var_export($raw, true) . ' | resolved=' . $value);
$db->setSetting($key, $value); $db->setSetting($key, $value);
$newValues[$key] = $value; $newValues[$key] = $value;
} }
@@ -74,10 +89,14 @@ if ($section === 'formulaire_restrictions') {
App::flash('success', "Degrés d'ouverture mis à jour."); App::flash('success', "Degrés d'ouverture mis à jour.");
} }
} elseif ($section === 'objet_types') { } elseif ($section === 'objet_types') {
$rawThese = $_POST['objet_these_enabled'] ?? '(missing)';
$rawFrart = $_POST['objet_frart_enabled'] ?? '(missing)';
$newValues = [ $newValues = [
'objet_these_enabled' => empty($_POST['objet_these_enabled']) ? '0' : '1', 'objet_these_enabled' => empty($_POST['objet_these_enabled']) ? '0' : '1',
'objet_frart_enabled' => empty($_POST['objet_frart_enabled']) ? '0' : '1', 'objet_frart_enabled' => empty($_POST['objet_frart_enabled']) ? '0' : '1',
]; ];
error_log('[settings.php] SAVE objet_types | objet_these_enabled raw=' . var_export($rawThese, true) . ' | resolved=' . $newValues['objet_these_enabled']);
error_log('[settings.php] SAVE objet_types | objet_frart_enabled raw=' . var_export($rawFrart, true) . ' | resolved=' . $newValues['objet_frart_enabled']);
$db->setSetting('objet_these_enabled', $newValues['objet_these_enabled']); $db->setSetting('objet_these_enabled', $newValues['objet_these_enabled']);
$db->setSetting('objet_frart_enabled', $newValues['objet_frart_enabled']); $db->setSetting('objet_frart_enabled', $newValues['objet_frart_enabled']);
$logger->logObjetTypesUpdate($newValues); $logger->logObjetTypesUpdate($newValues);

View File

@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<rect x="1" y="1" width="6" height="22" rx="1">
<animate attributeName="opacity" values="0.3;1;0.3" dur="1s" repeatCount="indefinite" begin="0s"/>
</rect>
<rect x="9" y="1" width="6" height="22" rx="1">
<animate attributeName="opacity" values="0.3;1;0.3" dur="1s" repeatCount="indefinite" begin="0.2s"/>
</rect>
<rect x="17" y="1" width="6" height="22" rx="1">
<animate attributeName="opacity" values="0.3;1;0.3" dur="1s" repeatCount="indefinite" begin="0.4s"/>
</rect>
</svg>

After

Width:  |  Height:  |  Size: 582 B

View File

@@ -387,10 +387,10 @@ CREATE INDEX idx_thesis_tags_thesis ON thesis_tags(thesis_id);
-- VIEWS -- VIEWS
-- ============================================================================ -- ============================================================================
CREATE VIEW v_smtp_active AS CREATE VIEW IF NOT EXISTS v_smtp_active AS
SELECT * FROM smtp_settings WHERE id = 1; SELECT * FROM smtp_settings WHERE id = 1;
CREATE VIEW v_theses_full AS CREATE VIEW IF NOT EXISTS v_theses_full AS
SELECT SELECT
t.id, t.id,
t.identifier, t.identifier,
@@ -450,7 +450,7 @@ LEFT JOIN thesis_tags tt ON t.id = tt.thesis_id
LEFT JOIN tags tg ON tt.tag_id = tg.id LEFT JOIN tags tg ON tt.tag_id = tg.id
GROUP BY t.id; GROUP BY t.id;
CREATE VIEW v_theses_public AS CREATE VIEW IF NOT EXISTS v_theses_public AS
SELECT * FROM v_theses_full SELECT * FROM v_theses_full
WHERE is_published = 1; WHERE is_published = 1;

View File

@@ -740,6 +740,19 @@
+%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
+\\\\\\\ to: rxpvwzkt 7fac18bc "feat(admin): add htmx toast feedback for settings checkboxes in contenus.php" (rebased revision) +\\\\\\\ to: rxpvwzkt 7fac18bc "feat(admin): add htmx toast feedback for settings checkboxes in contenus.php" (rebased revision)
++ $linkName = $link['name'] ?? ''; ++ $linkName = $link['name'] ?? '';
++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: rxpvwzkt 7fac18bc "feat(admin): add htmx toast feedback for settings checkboxes in contenus.php" (rebased revision)
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
- $linkName = $link['name'] ?? '';
- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination)
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: pqnovwxx 48308820 "fix(production): fix multiple remote server errors from nginx logs" (rebased revision)
$linkName = $link['name'] ?? '';
$linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
$linkLockedYear = $link['locked_year'] ?? null;
+%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
+\\\\\\\ to: pqnovwxx eb519770 "fix(production): fix multiple remote server errors from nginx logs" (rebased revision)
++ $linkName = $link['name'] ?? '';
++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; ++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
?> ?>
<tr class="admin-table-row" onclick="event.stopPropagation(); window.open('/partage/<?= urlencode($link['slug']) ?>', '_blank')" style="cursor:pointer"> <tr class="admin-table-row" onclick="event.stopPropagation(); window.open('/partage/<?= urlencode($link['slug']) ?>', '_blank')" style="cursor:pointer">

View File

@@ -99,7 +99,9 @@
hx-trigger="change" hx-trigger="change"
hx-target="#restrictions-response" hx-target="#restrictions-response"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-include="#fieldset-restrictions"> hx-include="#fieldset-restrictions"
hx-on::before-request="console.log('[restrictions] sending checked=' + this.checked + ' POST keys will include all #fieldset-restrictions inputs')"
hx-on::after-request="console.log('[restrictions] response received')">
<span> <span>
<strong>Activer la restriction d'accès</strong><br> <strong>Activer la restriction d'accès</strong><br>
<small>Pour les TFE de type "Interne", masquer les fichiers et exiger une demande d'accès par email. Les métadonnées et le résumé restent visibles publiquement.</small> <small>Pour les TFE de type "Interne", masquer les fichiers et exiger une demande d'accès par email. Les métadonnées et le résumé restent visibles publiquement.</small>
@@ -125,7 +127,9 @@
hx-trigger="change" hx-trigger="change"
hx-target="#acces-response" hx-target="#acces-response"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-include="#fieldset-acces"> hx-include="#fieldset-acces"
hx-on::before-request="console.log('[acces-libre] sending checked=' + this.checked)"
hx-on::after-request="console.log('[acces-libre] response received')">
<span> <span>
<strong>Libre</strong><br> <strong>Libre</strong><br>
<small>Libre accès — TFE accessible publiquement sur la plateforme et en bibliothèque</small> <small>Libre accès — TFE accessible publiquement sur la plateforme et en bibliothèque</small>
@@ -139,7 +143,9 @@
hx-trigger="change" hx-trigger="change"
hx-target="#acces-response" hx-target="#acces-response"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-include="#fieldset-acces"> hx-include="#fieldset-acces"
hx-on::before-request="console.log('[acces-interne] sending checked=' + this.checked)"
hx-on::after-request="console.log('[acces-interne] response received')">
<span> <span>
<strong>Interne</strong><br> <strong>Interne</strong><br>
<small>TFE accessible uniquement sur place en physique</small> <small>TFE accessible uniquement sur place en physique</small>
@@ -153,7 +159,9 @@
hx-trigger="change" hx-trigger="change"
hx-target="#acces-response" hx-target="#acces-response"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-include="#fieldset-acces"> hx-include="#fieldset-acces"
hx-on::before-request="console.log('[acces-interdit] sending checked=' + this.checked)"
hx-on::after-request="console.log('[acces-interdit] response received')">
<span> <span>
<strong>Interdit</strong><br> <strong>Interdit</strong><br>
<small>TFE non disponible en physique ni sur le site</small> <small>TFE non disponible en physique ni sur le site</small>
@@ -188,7 +196,9 @@
hx-trigger="change" hx-trigger="change"
hx-target="#types-response" hx-target="#types-response"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-include="#fieldset-types"> hx-include="#fieldset-types"
hx-on::before-request="console.log('[types-these] sending checked=' + this.checked)"
hx-on::after-request="console.log('[types-these] response received')">
<span> <span>
<strong>Thèse</strong><br> <strong>Thèse</strong><br>
<small>Thèses doctorales</small> <small>Thèses doctorales</small>
@@ -202,7 +212,9 @@
hx-trigger="change" hx-trigger="change"
hx-target="#types-response" hx-target="#types-response"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-include="#fieldset-types"> hx-include="#fieldset-types"
hx-on::before-request="console.log('[types-frart] sending checked=' + this.checked)"
hx-on::after-request="console.log('[types-frart] response received')">
<span> <span>
<strong>Frart</strong><br> <strong>Frart</strong><br>
<small>Formation de recherche en art</small> <small>Formation de recherche en art</small>

View File

@@ -15,6 +15,15 @@
<?php endif; ?> <?php endif; ?>
<script src="/assets/js/htmx.min.js"></script> <script src="/assets/js/htmx.min.js"></script>
<script> <script>
// Global HTMX debugging for settings checkboxes
document.body.addEventListener('htmx:sendError', function (e) {
console.error('[htmx:sendError] target=', e.target.id, 'detail=', e.detail);
});
document.body.addEventListener('htmx:beforeSend', function (e) {
if (e.target.id && (e.target.id.includes('fieldset-') || e.target.name)) {
console.log('[htmx:beforeSend] name=' + e.target.name + ' checked=' + e.target.checked, 'formData keys:', Array.from(new FormData(e.target.closest('fieldset'))).map(function(kv){return kv[0]}));
}
});
document.body.addEventListener('htmx:afterSettle', function (e) { document.body.addEventListener('htmx:afterSettle', function (e) {
if (e.target && e.target.id === 'toast-region') { if (e.target && e.target.id === 'toast-region') {
var warn = e.target.querySelector('.toast--warning'); var warn = e.target.querySelector('.toast--warning');

View File

@@ -68,6 +68,8 @@ deploy:
ssh xamxam "cd /var/www/xamxam && php -r 'if (!file_exists(\"/var/www/xamxam/storage/xamxam.db\")) { \$db = new PDO(\"sqlite:/var/www/xamxam/storage/xamxam.db\"); \$db->exec(file_get_contents(\"/var/www/xamxam/storage/schema.sql\")); echo \"Database created from schema.\\n\"; } else { echo \"Database already exists.\\n\"; }'" ssh xamxam "cd /var/www/xamxam && php -r 'if (!file_exists(\"/var/www/xamxam/storage/xamxam.db\")) { \$db = new PDO(\"sqlite:/var/www/xamxam/storage/xamxam.db\"); \$db->exec(file_get_contents(\"/var/www/xamxam/storage/schema.sql\")); echo \"Database created from schema.\\n\"; } else { echo \"Database already exists.\\n\"; }'"
# Run pending migrations # Run pending migrations
ssh xamxam "cd /var/www/xamxam && bash scripts/migrate.sh" ssh xamxam "cd /var/www/xamxam && bash scripts/migrate.sh"
# Deploy nginx configuration
@just deploy-nginx
# Sync .env separately (excluded above to avoid accidental overwrite on subsequent deploys) # Sync .env separately (excluded above to avoid accidental overwrite on subsequent deploys)
@just deploy-env @just deploy-env
@just deploy-verify-permissions @just deploy-verify-permissions
@@ -226,6 +228,17 @@ deploy-verify-permissions:
exit 1 exit 1
fi fi
[group('deploy')]
deploy-nginx:
# Upload nginx config to the server, test it, and reload.
# Uses the scripts/deploy-server.sh helper that handles the nginx
# config installation and reload (steps 2-4).
@echo "📋 Deploying nginx configuration…"
rsync -v nginx/xamxam.conf xamxam:/tmp/xamxam.conf
rsync -v scripts/deploy-server.sh xamxam:/tmp/deploy-server.sh
ssh xamxam "sudo DEPLOY_USER=\$USER bash /tmp/deploy-server.sh"
ssh xamxam "rm -f /tmp/deploy-server.sh /tmp/xamxam.conf"
[group('deploy')] [group('deploy')]
deploy-script script_name: deploy-script script_name:
# Generic script deployer (e.g., just deploy-script setup-server) # Generic script deployer (e.g., just deploy-script setup-server)

View File

@@ -7,8 +7,10 @@ limit_req_zone $binary_remote_addr zone=general:10m rate=30r/m;
limit_req_zone $binary_remote_addr zone=search:10m rate=30r/m; limit_req_zone $binary_remote_addr zone=search:10m rate=30r/m;
# Admin: already protected by HTTP Basic Auth; rate limiting here only guards # Admin: already protected by HTTP Basic Auth; rate limiting here only guards
# against brute-force on the auth layer, not normal browsing. # against brute-force on the auth layer, not normal browsing.
# 60r/m = 1r/s sustained, burst=20 covers rapid page navigation. # Contenu.php triggers ~12 concurrent HTMX GETs on page load, so we need a
limit_req_zone $binary_remote_addr zone=admin:10m rate=60r/m; # generous burst. 300r/m = 5r/s sustained, burst=30 handles all fragments
# without dropping any, while still limiting brute-force attempts.
limit_req_zone $binary_remote_addr zone=admin:10m rate=300r/m;
# Main server block # Main server block
server { server {
@@ -44,9 +46,9 @@ server {
# Server tokens already disabled in nginx.conf # Server tokens already disabled in nginx.conf
# server_tokens off; # server_tokens off;
# Max upload size (for thesis files) # Max upload size (for thesis files — can include video)
client_max_body_size 100M; client_max_body_size 256M;
client_body_timeout 120s; client_body_timeout 300s;
# Logging # Logging
access_log /var/log/nginx/xamxam_access.log; access_log /var/log/nginx/xamxam_access.log;
@@ -118,7 +120,9 @@ server {
auth_basic_user_file /etc/nginx/.htpasswd-xamxam; auth_basic_user_file /etc/nginx/.htpasswd-xamxam;
# Rate limiting for admin # Rate limiting for admin
limit_req zone=admin burst=20 nodelay; # 300r/m rate + burst=30 allows all concurrent HTMX fragments (up to ~12
# on contenus.php) while still capping brute-force at 5 req/s sustained.
limit_req zone=admin burst=30 nodelay;
# Content-Security-Policy - Admin policy # Content-Security-Policy - Admin policy
# script-src needs 'unsafe-inline' for the OverType editor init block # script-src needs 'unsafe-inline' for the OverType editor init block
@@ -133,12 +137,12 @@ server {
include snippets/fastcgi-php.conf; include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.4-fpm.sock; fastcgi_pass unix:/var/run/php/php8.4-fpm.sock;
# Security parameters # Security parameters (must be <= client_max_body_size)
fastcgi_param PHP_VALUE "upload_max_filesize=50M \n post_max_size=100M"; fastcgi_param PHP_VALUE "upload_max_filesize=256M \n post_max_size=256M";
# Timeouts # Timeouts
fastcgi_read_timeout 120; fastcgi_read_timeout 300;
fastcgi_send_timeout 120; fastcgi_send_timeout 300;
} }
# Additional security headers for admin # Additional security headers for admin
@@ -173,12 +177,12 @@ server {
include snippets/fastcgi-php.conf; include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.4-fpm.sock; fastcgi_pass unix:/var/run/php/php8.4-fpm.sock;
# Security parameters # Security parameters (must be <= client_max_body_size)
fastcgi_param PHP_VALUE "upload_max_filesize=50M \n post_max_size=100M"; fastcgi_param PHP_VALUE "upload_max_filesize=256M \n post_max_size=256M";
# Timeouts # Timeouts
fastcgi_read_timeout 120; fastcgi_read_timeout 300;
fastcgi_send_timeout 120; fastcgi_send_timeout 300;
} }
# All other clean URLs — fall through to front controller # All other clean URLs — fall through to front controller