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:
@@ -1138,6 +1138,72 @@ Update one or more settings. Unknown keys are silently ignored.
|
||||
|
||||
---
|
||||
|
||||
## Maintenance Mode
|
||||
|
||||
A single switch that takes the **whole platform** offline — the public site, the admin SPA's data calls, every `/api/v1/*` endpoint, and therefore `nobat724_front` and `clinic-pro-tauri` too. Enforced centrally by `App\Shared\EventSubscriber\MaintenanceSubscriber`; no controller checks it itself.
|
||||
|
||||
### Settings keys
|
||||
|
||||
Managed through the same `GET`/`PATCH /api/v1/admin/settings` endpoints (whitelisted in `SiteConfigController::ALLOWED_KEYS`). Admin panel: `/admin/settings` → بخش «حالت تعمیرات».
|
||||
|
||||
| Key | Default | Description |
|
||||
|---|---|---|
|
||||
| `maintenance_enabled` | `"0"` | `"1"`/`"true"`/`"on"`/`"yes"` = maintenance active |
|
||||
| `maintenance_title` | `در حال بهروزرسانی سیستم` | Heading of the HTML maintenance page |
|
||||
| `maintenance_message` | `سامانه موقتاً ...` | Shown both on the HTML page and as the API error `message` |
|
||||
| `maintenance_retry_after` | `"600"` | Seconds; sent as the `Retry-After` response header |
|
||||
| `maintenance_allowed_ips` | `""` | Comma-separated IPs that bypass maintenance without logging in |
|
||||
|
||||
Changing any `maintenance_*` key invalidates the 30-second `MaintenanceService` cache immediately, so a toggle takes effect on the next request.
|
||||
|
||||
### Behaviour while enabled
|
||||
|
||||
**API requests** (path starts with `/api/`, or `Accept: application/json`, or `X-Requested-With: XMLHttpRequest`):
|
||||
|
||||
```
|
||||
HTTP/1.1 503 Service Unavailable
|
||||
Retry-After: 600
|
||||
|
||||
{
|
||||
"success": false,
|
||||
"data": null,
|
||||
"errors": [
|
||||
{ "code": "MAINTENANCE_MODE", "message": "<maintenance_message>" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The envelope is identical to `BaseController::error()`, so existing clients parse it unchanged. Clients should detect maintenance by **both** `status === 503` **and** `errors[0].code === "MAINTENANCE_MODE"` — a bare 503 may come from a reverse proxy.
|
||||
|
||||
**Browser requests** — `templates/maintenance.html.twig` rendered with HTTP `503`, same `Retry-After` header, `noindex, nofollow`.
|
||||
|
||||
Both the `kernel.request` (priority 6) and `kernel.exception` (priority 20) paths are covered, so routing 404/405 and firewall 401 responses also return maintenance rather than leaking their normal errors.
|
||||
|
||||
### Who gets through
|
||||
|
||||
1. **Whitelisted paths** — never blocked, in this order of importance:
|
||||
`/oauth/*`, `/api/v1/user/{login,send-code,verify-code,otp-login}`, `/session/token` (admins must still be able to sign in), `/api/v1/admin/settings` (the only way to turn maintenance back off), `/health`, `/admin*` (the SPA shell HTML — its data calls are still guarded), `/build/*`, `/favicon.ico`, `/_wdt`, `/_profiler`.
|
||||
2. **`maintenance_allowed_ips`** — exact client-IP match.
|
||||
3. **`ROLE_ADMIN`** — resolved from the firewall token. Several admin-panel endpoints (`/api/v1/doctors`, `/api/v1/categorys/*`, …) live in the `public_endpoints` firewall with `security: false`, where no token is ever resolved; for those the subscriber falls back to decoding the `Authorization: Bearer` JWT and checking its `roles` claim. An invalid or forged token does not bypass.
|
||||
|
||||
Every other role — `ROLE_DOCTOR`, `ROLE_CLINIC`, `ROLE_SECRETARY`, `ROLE_REPRESENTATION` — is blocked.
|
||||
|
||||
### Console escape hatch
|
||||
|
||||
If the admin panel is unreachable:
|
||||
|
||||
```bash
|
||||
ddev exec php bin/console app:maintenance status
|
||||
ddev exec php bin/console app:maintenance on
|
||||
ddev exec php bin/console app:maintenance off
|
||||
```
|
||||
|
||||
### Failure behaviour
|
||||
|
||||
`MaintenanceService` is **fail-open**: if Redis is unavailable it reads straight from the database, and if the database also fails it reports maintenance as disabled. This layer must never become the cause of an outage.
|
||||
|
||||
---
|
||||
|
||||
## Pre-Registration Management
|
||||
|
||||
### GET `/api/v1/admin/pre-registrations`
|
||||
|
||||
Reference in New Issue
Block a user