feat(audit): add Audit Log module — coverage, metrics endpoint, indexes
- UserStatusView, EventModerationView, ReviewModerationView, PartnerKYCReviewView: each state change now emits _audit_log() inside the same transaction.atomic() block so the log stays consistent with DB state on partial failure - AuditLogMetricsView: GET /api/v1/rbac/audit-log/metrics/ returns total/today/week/distinct_users/by_action_group; 60 s cache with ?nocache=1 bypass - AuditLogListView: free-text search (Q over action/target/user), page_size bounded to [1, 200] - accounts.User.ALL_MODULES += 'audit-log'; StaffProfile.SCOPE_TO_MODULE['audit'] = 'audit-log' - Migration 0005: composite indexes (action,-created_at) and (target_type,target_id) on AuditLog - admin_api/tests.py: 11 tests covering list shape, search, page bounds, metrics shape+nocache, suspend/ban/reinstate audit emission Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
63
CHANGELOG.md
63
CHANGELOG.md
@@ -5,6 +5,69 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), version
|
||||
|
||||
---
|
||||
|
||||
## [1.12.0] — 2026-04-21
|
||||
|
||||
### Added
|
||||
- **Audit coverage for four moderation endpoints** — every admin state change now leaves a matching row in `AuditLog`, written in the same `transaction.atomic()` block as the state change so the log can never disagree with the database:
|
||||
- `UserStatusView` (`PATCH /api/v1/users/<id>/status/`) — `user.suspended`, `user.banned`, `user.reinstated`, `user.flagged`; details capture `reason`, `previous_status`, `new_status`
|
||||
- `EventModerationView` (`PATCH /api/v1/events/<id>/moderate/`) — `event.approved`, `event.rejected`, `event.flagged`, `event.featured`, `event.unfeatured`; details include `reason`, `partner_id`, `previous_status`/`new_status`, `previous_is_featured`/`new_is_featured`
|
||||
- `ReviewModerationView` (`PATCH /api/v1/reviews/<id>/moderate/`) — `review.approved`, `review.rejected`, `review.edited`; details include `reject_reason`, `edited_text` flag, `original_text` on edits
|
||||
- `PartnerKYCReviewView` (`POST /api/v1/partners/<id>/kyc/review/`) — `partner.kyc.approved`, `partner.kyc.rejected`, `partner.kyc.requested_info` (new `requested_info` decision leaves compliance state intact and only records the info request)
|
||||
- **`GET /api/v1/rbac/audit-log/metrics/`** — `AuditLogMetricsView` returns `total`, `today`, `week`, `distinct_users`, and a `by_action_group` breakdown (`create`/`update`/`delete`/`moderate`/`auth`/`other`). Cached 60 s under key `admin_api:audit_log:metrics:v1`; pass `?nocache=1` to bypass (useful from the Django shell during incident response)
|
||||
- **`GET /api/v1/rbac/audit-log/`** — free-text `search` parameter (Q-filter over `action`, `target_type`, `target_id`, `user__username`, `user__email`); `page_size` now bounded to `[1, 200]` with defensive fallback to defaults on non-integer input
|
||||
- **`accounts.User.ALL_MODULES`** — appended `audit-log`; `StaffProfile.get_allowed_modules()` adds `'audit'` → `'audit-log'` to `SCOPE_TO_MODULE` so scope-based staff resolve the module correctly
|
||||
- **`admin_api/migrations/0005_auditlog_indexes.py`** — composite indexes `(action, -created_at)` and `(target_type, target_id)` on `AuditLog` to keep the /audit-log page fast past ~10k rows; reversible via Django's default `RemoveIndex` reverse op
|
||||
- **`admin_api/tests.py`** — `AuditLogListViewTests`, `AuditLogMetricsViewTests`, `UserStatusAuditEmissionTests` covering list shape, search, pagination bounds, metrics shape + `nocache`, and audit emission on suspend / ban / reinstate
|
||||
|
||||
### Deploy notes
|
||||
Admin users created before this release won't have `audit-log` in their `allowed_modules` TextField. Backfill with:
|
||||
```python
|
||||
# Django shell
|
||||
from accounts.models import User
|
||||
for u in User.objects.filter(role__in=['admin', 'manager']):
|
||||
mods = [m.strip() for m in (u.allowed_modules or '').split(',') if m.strip()]
|
||||
if 'audit-log' not in mods and mods: # only touch users with explicit lists
|
||||
u.allowed_modules = ','.join(mods + ['audit-log'])
|
||||
u.save(update_fields=['allowed_modules'])
|
||||
```
|
||||
Users on the implicit full-access list (empty `allowed_modules` + admin role) pick up the new module automatically via `get_allowed_modules()`.
|
||||
|
||||
---
|
||||
|
||||
## [1.11.0] — 2026-04-12
|
||||
|
||||
### Added
|
||||
- **Worldline Connect payment integration** (`banking_operations/worldline/`)
|
||||
- `client.py` — `WorldlineClient`: HMAC-SHA256 signed requests, `create_hosted_checkout()`, `get_hosted_checkout_status()`, `verify_webhook_signature()`
|
||||
- `views.py` — `POST /api/payments/webhook/` (CSRF-exempt, signature-verified Worldline server callback) + `POST /api/payments/verify/` (frontend polls on return URL)
|
||||
- `emails.py` — HTML ticket confirmation email with per-ticket QR codes embedded as base64 inline images
|
||||
- `WorldlineOrder` model in `banking_operations/models.py` — tracks each hosted-checkout session (hosted_checkout_id, reference_id, status, raw_response, webhook_payload)
|
||||
- **`Booking.payment_status`** field — `pending / paid / failed / cancelled` (default `pending`); migration `bookings/0002_booking_payment_status`
|
||||
- **`banking_operations/services.py::transaction_initiate`** — implemented (was a stub); calls Worldline API, creates `WorldlineOrder`, returns `payment_url` back to `CheckoutAPI`
|
||||
- **Settings**: `WORLDLINE_MERCHANT_ID`, `WORLDLINE_API_KEY_ID`, `WORLDLINE_API_SECRET_KEY`, `WORLDLINE_WEBHOOK_SECRET_KEY`, `WORLDLINE_API_ENDPOINT` (default: sandbox), `WORLDLINE_RETURN_URL`
|
||||
- **Requirements**: `requests>=2.31.0`, `qrcode[pil]>=7.4.2`
|
||||
|
||||
### Flow
|
||||
1. User adds tickets to cart → `POST /api/bookings/checkout/` creates Bookings + calls `transaction_initiate`
|
||||
2. `transaction_initiate` creates `WorldlineOrder` + calls Worldline → returns redirect URL
|
||||
3. Frontend redirects user to Worldline hosted checkout page
|
||||
4. After payment, Worldline redirects to `WORLDLINE_RETURN_URL` (`app.eventifyplus.com/booking/confirm?hostedCheckoutId=...`)
|
||||
5. SPA calls `POST /api/payments/verify/` — checks local status; if still pending, polls Worldline API directly
|
||||
6. Worldline webhook fires `POST /api/payments/webhook/` → generates Tickets (one per quantity), marks Booking `paid`, sends confirmation email with QR codes
|
||||
7. Partner scans QR code at event → existing `POST /api/bookings/check-in/` marks `Ticket.is_checked_in=True`
|
||||
|
||||
### Deploy requirement
|
||||
Set in Django container `.env`:
|
||||
```
|
||||
WORLDLINE_MERCHANT_ID=...
|
||||
WORLDLINE_API_KEY_ID=...
|
||||
WORLDLINE_API_SECRET_KEY=...
|
||||
WORLDLINE_WEBHOOK_SECRET_KEY=...
|
||||
```
|
||||
`WORLDLINE_API_ENDPOINT` defaults to sandbox — set to production URL when going live.
|
||||
|
||||
---
|
||||
|
||||
## [1.10.0] — 2026-04-10
|
||||
|
||||
### Security
|
||||
|
||||
Reference in New Issue
Block a user