feat(backend): add deprecation middleware and API versioning support

- Add deprecation middleware for warning headers on sunset endpoints
- Add jails_v2 router for API v2 migration path
- Update CI workflow with new test coverage
- Update API versioning documentation
- Remove completed tasks from Tasks.md
This commit is contained in:
2026-05-04 00:03:52 +02:00
parent c8b48b5b65
commit 65fe747cba
8 changed files with 409 additions and 39 deletions

View File

@@ -23,8 +23,8 @@ This document explains when and how to version endpoints, how deprecation works,
/api/v{major}/<resource>/<path>
```
- **v1** — current version (2026-05-02)
- **v2** — reserved for future breaking changes
- **v1** — current version (released 2026-05-02)
- **v2** — reserved; skeleton router deployed at `/api/v2/jails` but **not yet active** for production traffic
- **PATCH** versions (v1.1, v1.2) are **not** used; only **major** version bumps indicate breaking changes
- The OpenAPI schema is always available at `/api/openapi.json` regardless of version
@@ -56,14 +56,35 @@ These do **not** require a version bump.
When an endpoint is deprecated:
1. The endpoint **remains functional** for a minimum of **6 months** from the `Sunset` date
2. Response headers are added:
2. Response headers are added to every 2xx response:
```
Deprecation: true
Sunset: <RFC-5322 date>
Link: <https://bangui.example.com/api/v2/...>; rel="successor-version"
```
3. The OpenAPI schema marks the endpoint with `deprecated: true`
4. Documentation is updated to show the endpoint as deprecated
3. The endpoint is registered in the deprecation middleware (``app/middleware/deprecation.py``)
4. The OpenAPI schema marks the endpoint with `deprecated: true`
5. Documentation is updated to show the endpoint as deprecated
### Implementing Deprecation Headers
The ``DeprecationHeaderMiddleware`` (``app/middleware/deprecation.py``) automatically injects
the correct headers for any registered deprecated endpoint. To schedule an endpoint for removal:
```python
from datetime import datetime, timezone, timedelta
from app.middleware.deprecation import register_deprecated_endpoint
# Example: deprecate /api/v1/jails on 2026-11-03 (6 months from v2 release)
register_deprecated_endpoint(
path_prefix="/api/v1/jails",
sunset_date=datetime(2026, 11, 3, tzinfo=timezone.utc),
successor_url="/api/v2/jails",
)
```
The middleware runs on every response; if the request path matches a registered deprecated prefix,
the appropriate headers are appended before the response is returned.
---
@@ -81,19 +102,21 @@ router = APIRouter(prefix="/api/v1/my-resource", tags=["My Resource"])
1. Create a new router file (e.g., `routers/my_resource_v2.py`) with the v2 prefix:
```python
router = APIRouter(prefix="/api/v2/my-resource", tags=["My Resource"])
router = APIRouter(prefix="/api/v2/my-resource", tags=["My Resource (v2)"])
```
2. Copy or adapt the v1 handler logic as needed
2. Copy or adapt the v1 handler logic as needed. Extract shared business logic into
a **service layer function** so both routers call the same underlying code.
3. Register the new router in `app/main.py`:
```python
app.include_router(my_resource_v2.router)
```
4. Add deprecation headers to the **old** v1 router by marking it deprecated in the OpenAPI spec
4. Register the v1 endpoint for deprecation headers (see §4 above)
5. Update this document to reflect the new version lifecycle
### Keeping routers DRY
If v1 and v2 share logic, extract business logic into a **service layer function** and call it from both router handlers. Routers should only contain HTTP concerns (parameters, responses, status codes).
Routers should only contain HTTP concerns (parameters, responses, status codes). Business logic
belongs in the service layer. Both v1 and v2 handlers can call the same service function.
---
@@ -107,6 +130,8 @@ const BASE_URL: string = import.meta.env.VITE_API_URL ?? "/api/v1";
All endpoint paths in `frontend/src/api/endpoints.ts` are defined as relative paths (e.g., `/bans`, `/jails`) and are appended to `BASE_URL` at runtime.
When v2 is released, update ``VITE_API_URL`` in the environment configuration to point to `/api/v2`.
---
## 7. OpenAPI / Documentation
@@ -118,8 +143,24 @@ All endpoint paths in `frontend/src/api/endpoints.ts` are defined as relative pa
---
## 8. Version History
## 8. CI Breaking-Change Checks
A GitHub Actions job runs on every pull request to detect breaking OpenAPI changes:
- ``openapi-breaking-changes`` job (PR only): generates the current OpenAPI spec and
compares it against the baseline committed on the last push to `main`. If any breaking
changes are found, the job fails and the PR cannot be merged.
- ``openapi-baseline-commit`` job (main push only): generates and commits the current
OpenAPI spec as the new baseline for future PR comparisons.
To trigger the baseline update, push to main after merging a version bump or any change
that legitimately alters the OpenAPI surface.
---
## 9. Version History
| Version | Status | Released | Sunset Date | Notes |
|---------|--------|---------|-------------|-------|
| v1 | **Current** | 2026-05-02 | — | Initial versioning; all endpoints moved from `/api/` to `/api/v1/` |
| v1 | **Current** | 2026-05-02 | — | Initial versioning; all endpoints moved from `/api/` to `/api/v1/` |
| v2 | **Reserved — skeleton active, endpoints not yet available** | — | — | Router skeleton at `app/routers/jails_v2.py`; real endpoints will be added before activation |

View File

@@ -1,30 +1,3 @@
### Issue #55: MEDIUM - Correlation ID Scope Is Module-Level (Resets on HMR)
**Where found**:
- `frontend/src/api/client.ts:26-39` `sessionCorrelationId` stored as a module variable
**Why this is needed**:
Hot Module Replacement (HMR) re-evaluates modules, generating a new correlation ID mid-session. Distributed traces lose continuity, making it impossible to correlate logs from the same user session.
**Goal**:
Persist the correlation ID across module re-evaluations for the lifetime of the browser tab.
**What to do**:
1. Store the correlation ID in `sessionStorage` on first generation; read from there on subsequent module evaluations.
2. Clear it on logout.
**Possible traps and issues**:
- `sessionStorage` is tab-local, which is the desired scope.
- Ensure the ID is not leaked in URLs or logs.
**Docs changes needed**:
- Add comment explaining the persistence strategy in `client.ts`.
**Doc references**:
- `frontend/src/api/client.ts` `getSessionCorrelationId()`
---
### Issue #56: MEDIUM - No API Versioning or Deprecation Strategy
**Where found**: