feat: platform admin backend — controllers, services, routes, tests
Add cross-organisation admin API endpoints behind role:super_admin middleware: - AdminOrganisationController: CRUD with search, filter, billing_status management - AdminUserController: user management with role assignment across orgs - AdminStatsController: platform-wide aggregate statistics - AdminActivityLogController: filterable activity log viewer - AdminImpersonationController + ImpersonationService: user impersonation with token-based session management and activity logging - BillingStatus enum, form requests, API resources, 23 feature tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
196
dev-docs/API.md
196
dev-docs/API.md
@@ -726,3 +726,199 @@ Additional filter parameters on `GET /organisations/{org}/events/{event}/persons
|
||||
- `?has_preference=true` — only persons who submitted section preferences
|
||||
|
||||
_(Extend this contract per module as endpoints are implemented.)_
|
||||
|
||||
## Platform Admin
|
||||
|
||||
All admin endpoints require `auth:sanctum` + `role:super_admin`. They bypass OrganisationScope and query across all organisations.
|
||||
|
||||
Base path: `/api/v1/admin/`
|
||||
|
||||
### Admin Organisations
|
||||
|
||||
- `GET /admin/organisations` — list all organisations (paginated)
|
||||
- `GET /admin/organisations/{organisation}` — show with counts and total persons
|
||||
- `POST /admin/organisations` — not supported (405), use regular endpoint
|
||||
- `PUT /admin/organisations/{organisation}` — update name, slug, billing_status, settings
|
||||
- `DELETE /admin/organisations/{organisation}` — soft delete
|
||||
|
||||
#### Query Parameters (index)
|
||||
|
||||
- `search` — filter by name or slug (partial match)
|
||||
- `billing_status` — filter by billing status (`trial`, `active`, `suspended`, `cancelled`)
|
||||
- `sort` — sort field: `name` (default), `created_at`
|
||||
- `direction` — sort direction: `asc` (default), `desc`
|
||||
|
||||
#### AdminOrganisationResource
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "01JXYZ...",
|
||||
"name": "Festival Corp",
|
||||
"slug": "festival-corp",
|
||||
"billing_status": "active",
|
||||
"billing_status_label": "Active",
|
||||
"settings": {},
|
||||
"events_count": 5,
|
||||
"users_count": 12,
|
||||
"total_persons": 342,
|
||||
"created_at": "2026-01-15T10:00:00+00:00",
|
||||
"updated_at": "2026-04-10T12:00:00+00:00",
|
||||
"deleted_at": null
|
||||
}
|
||||
```
|
||||
|
||||
#### Update Body
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Festival Corp",
|
||||
"slug": "festival-corp",
|
||||
"billing_status": "suspended",
|
||||
"settings": {}
|
||||
}
|
||||
```
|
||||
|
||||
### Admin Users
|
||||
|
||||
- `GET /admin/users` — list all users with organisation memberships (paginated)
|
||||
- `GET /admin/users/{user}` — show with organisations and roles
|
||||
- `PUT /admin/users/{user}` — update name, email, timezone, locale, platform roles
|
||||
- `DELETE /admin/users/{user}` — soft delete + revoke all tokens
|
||||
|
||||
#### Query Parameters (index)
|
||||
|
||||
- `search` — filter by first_name, last_name, or email (partial match)
|
||||
- `organisation_id` — filter by organisation membership
|
||||
- `role` — filter by Spatie role name
|
||||
|
||||
#### AdminUserResource
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "01JXYZ...",
|
||||
"first_name": "Jan",
|
||||
"last_name": "de Vries",
|
||||
"full_name": "Jan de Vries",
|
||||
"email": "jan@example.nl",
|
||||
"avatar": null,
|
||||
"timezone": "Europe/Amsterdam",
|
||||
"locale": "nl",
|
||||
"email_verified_at": "2026-01-15T10:00:00+00:00",
|
||||
"created_at": "2026-01-15T10:00:00+00:00",
|
||||
"roles": ["super_admin"],
|
||||
"is_super_admin": true,
|
||||
"organisations": [
|
||||
{ "id": "01JXYZ...", "name": "Festival Corp", "slug": "festival-corp", "role": "org_admin" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Update Body
|
||||
|
||||
```json
|
||||
{
|
||||
"first_name": "Jan",
|
||||
"last_name": "de Vries",
|
||||
"email": "jan@example.nl",
|
||||
"timezone": "Europe/Amsterdam",
|
||||
"locale": "nl",
|
||||
"roles": ["super_admin"]
|
||||
}
|
||||
```
|
||||
|
||||
`roles` accepts platform-level roles only: `super_admin`, `support_agent`. Organisation/event roles are managed via the regular endpoints.
|
||||
|
||||
### Admin Stats
|
||||
|
||||
- `GET /admin/stats` — platform-wide aggregate counts
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"organisations": {
|
||||
"total": 15,
|
||||
"by_billing_status": { "trial": 3, "active": 10, "suspended": 1, "cancelled": 1 }
|
||||
},
|
||||
"events": {
|
||||
"total": 42,
|
||||
"by_status": { "draft": 10, "published": 8, "registration_open": 12, "showday": 5, "closed": 7 }
|
||||
},
|
||||
"users": {
|
||||
"total": 156,
|
||||
"verified": 142
|
||||
},
|
||||
"persons": {
|
||||
"total": 2340
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Admin Activity Log
|
||||
|
||||
- `GET /admin/activity-log` — paginated activity log (25 per page)
|
||||
|
||||
#### Query Parameters
|
||||
|
||||
- `causer_id` — filter by user who caused the action
|
||||
- `subject_type` — filter by subject model type
|
||||
- `log_name` — filter by log name (e.g. `admin`, `default`)
|
||||
- `from` — filter from date (ISO 8601)
|
||||
- `to` — filter to date (ISO 8601)
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"log_name": "admin",
|
||||
"description": "Updated organisation Festival Corp",
|
||||
"event": "admin.organisation.updated",
|
||||
"causer": { "id": "01JXYZ...", "name": "Super Admin", "email": "admin@crewli.app" },
|
||||
"subject_type": "App\\Models\\Organisation",
|
||||
"subject_id": "01JXYZ...",
|
||||
"properties": { "billing_status": "suspended" },
|
||||
"created_at": "2026-04-14T10:00:00+00:00"
|
||||
}
|
||||
],
|
||||
"meta": { "current_page": 1, "last_page": 1, "per_page": 25, "total": 1 }
|
||||
}
|
||||
```
|
||||
|
||||
### Admin Impersonation
|
||||
|
||||
- `POST /admin/impersonate/{user}` — start impersonating a user (requires `role:super_admin`)
|
||||
- `POST /admin/stop-impersonation` — stop impersonation (requires `auth:sanctum` only, callable by impersonated user)
|
||||
|
||||
#### Start Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"token": "1|abc123...",
|
||||
"user": { "...AdminUserResource..." },
|
||||
"admin_id": "01JXYZ..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Stop Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"user": { "...AdminUserResource (original admin)..." }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Business Rules
|
||||
|
||||
- Cannot impersonate another super_admin (403)
|
||||
- Impersonation token has name `impersonation-by-{admin_id}`
|
||||
- Admin ID is cached for 4 hours at key `impersonation:{token_id}`
|
||||
- Activity log records both start (`admin.impersonation.started`) and stop (`admin.impersonation.stopped`)
|
||||
|
||||
Reference in New Issue
Block a user