feat(config): add central maintenance mode

Adds a platform-wide maintenance switch controlled from the admin panel.
A single kernel.request subscriber (priority 6, after the firewall listener)
short-circuits every request with 503, so no controller has to check it and
all API clients — the admin SPA, nobat724_front and clinic-pro-tauri — are
covered at once.

- SiteConfig gains five maintenance_* keys; no entity change, no migration
- MaintenanceService caches the state in Redis for 30s and is fail-open:
  a Redis or database failure never takes the site down by itself
- API responses reuse the BaseController::error() envelope with code
  MAINTENANCE_MODE plus a Retry-After header; browsers get a self-contained
  Twig page (inline CSS, noindex) that renders even mid-deploy
- Whitelist keeps /oauth/*, the login endpoints and /api/v1/admin/settings
  reachable, otherwise an admin could neither sign in nor switch it back off
- Admin bypass falls back to decoding the Authorization JWT, because several
  admin-panel endpoints sit in the public_endpoints firewall (security: false)
  where no token is ever resolved and isGranted always returns false
- A kernel.exception handler at priority 20 covers routing 404/405 and
  firewall 401, which are thrown before the request listener runs
- app:maintenance on|off|status is the escape hatch when the panel is down

Also removes a stray `APP_SECRET = ...` line from .env.dev: the spaces around
`=` are rejected by Symfony Dotenv, which made every console command and the
whole app fatal. The secret already lives in .env.local, as the comment above
that line instructs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-07-19 22:01:34 +03:30
co-authored by Claude Fable 5
parent 6275b3da1e
commit 7ac8ddbd25
11 changed files with 1005 additions and 2 deletions
@@ -30,6 +30,13 @@ class SiteConfigRepository extends ServiceEntityRepository
'mellat_username' => '',
'mellat_password' => '',
'sep_terminal_id' => '',
// maintenance mode — کنترل مرکزی در MaintenanceSubscriber
'maintenance_enabled' => '0',
'maintenance_title' => 'در حال به‌روزرسانی سیستم',
'maintenance_message' => 'سامانه موقتاً برای انجام عملیات فنی در دسترس نیست. لطفاً چند دقیقه دیگر مجدداً تلاش کنید.',
'maintenance_retry_after' => '600',
// CSV؛ IPهایی که حتی در حالت تعمیرات دسترسی کامل دارند
'maintenance_allowed_ips' => '',
];
public function __construct(ManagerRegistry $registry)