API — Administration¶
This page documents AdminController and its AdminAuth helper, mounted under /api/admin. All endpoints on this page require admin authentication — either via session cookie (after POST /api/admin/login) or via the X-Admin-Token header.
Controllers covered
AdminController→ admin login/logout, product, user, order, admin account management, image uploadAdminAuth(static helper) → centralizes the admin auth check (isAdmin(session, token))
The hidden admin portal (/api/admin/portal/*) is documented separately in admin-portal.md.
Admin authentication mechanism¶
All AdminController endpoints (except login and status) are protected by the same check:
// Checks: session cookie OR X-Admin-Token header
if (!isAdmin(session, token)) return unauthorized();
Session cookie:
POST /api/admin/login→ storesadmin_logged_in = truein theHttpSession- Subsequent requests → Spring automatically transmits
JSESSIONID AdminAuth.isAdmin()reads the session key
X-Admin-Token header:
POST /api/admin/login→ also returns a UUIDadminTokenin the body- The token is stored in memory in
AdminController.VALID_ADMIN_TOKENS(ConcurrentHashMap) - Subsequent requests → the client sends
X-Admin-Token: <uuid> AdminAuth.isAdmin()checks the token's presence in the map
Non-persistent tokens
The VALID_ADMIN_TOKENS map is reset on every backend restart. This is intentional — pedagogical behavior for memory chaos (heap dump) demos. In a training environment, a backend shutdown forces all admins to reconnect.
SuperAdmin vs ordinary Admin¶
PerfShop distinguishes two administrator levels:
- Superadmin (
isSuperAdmin = trueflag inadmin_users): full access, including admin account management. A single superadmin exists by default, created by the initial seed. - Ordinary admin: limited access according to granular rights (
canAccessChaos,canAccessMonitoring,canAccessAdmin,canAccessJmeter,canAccessScripts).
Endpoints marked 👑 in the tables below are reserved for superadmin — they return 403 Forbidden with the message Reserved for superadmin. for an ordinary admin.
Overview¶
Authentication¶
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST |
/api/admin/login |
None | Admin login (BCrypt on admin_users) |
POST |
/api/admin/logout |
Session or token | Destroys the session and invalidates the token |
GET |
/api/admin/status |
None (read) | Authentication status |
Admin profile¶
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/api/admin/accounts/me |
Admin | Profile of the logged-in account |
Admin account management¶
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/api/admin/accounts |
Superadmin 👑 | List of all admin accounts |
POST |
/api/admin/accounts |
Superadmin 👑 | Create an admin account |
DELETE |
/api/admin/accounts/{id} |
Superadmin 👑 | Delete an admin account |
PUT |
/api/admin/accounts/{id}/password |
Admin (self) or Superadmin 👑 | Change a password |
PUT |
/api/admin/accounts/{id}/rights |
Superadmin 👑 | Modify access rights |
Products¶
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/api/admin/products |
Admin | List of non-pedagogical products |
POST |
/api/admin/products |
Admin | Create a product |
PUT |
/api/admin/products/{id} |
Admin | Update a product |
DELETE |
/api/admin/products/{id} |
Admin | Delete a product |
POST |
/api/admin/products/{id}/image |
Admin | Product image upload (multipart) |
Users¶
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/api/admin/users |
Admin | User list with order counter |
POST |
/api/admin/users |
Admin | Create a user |
DELETE |
/api/admin/users/{id} |
Admin | Delete a user (and their orders) |
Orders¶
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/api/admin/orders |
Admin | List of all orders |
DELETE |
/api/admin/orders/{id} |
Admin | Delete an order |
Admin authentication¶
POST /api/admin/login¶
Auth: none (this is the entry point)
Service: AdminUserService.authenticate() (BCrypt against the admin_users table)
Request¶
Response — 200 OK¶
{
"success": true,
"adminToken": "550e8400-e29b-41d4-a716-446655440000",
"email": "admin@perfshop.fr",
"isSuperAdmin": true,
"canAccessChaos": true,
"canAccessMonitoring": true,
"canAccessAdmin": true,
"canAccessJmeter": true,
"canAccessScripts": true
}
The canAccess* flags are used by the frontend to adapt the display (hide forbidden tabs). The adminToken is the same UUID stored in VALID_ADMIN_TOKENS — it can be used in place of the cookie.
Error codes¶
| Code | Body | Cause |
|---|---|---|
| 401 | {"error": "Invalid credentials"} |
Key auth.error.credentials |
POST /api/admin/logout¶
Destroys the session and removes the token from VALID_ADMIN_TOKENS.
Auth: session or token
Response — 200 OK¶
No error — the endpoint is idempotent (a double logout always returns 200).
GET /api/admin/status¶
Returns admin authentication state without generating an error.
Auth: optional
Response — 200 OK¶
Admin profile¶
GET /api/admin/accounts/me¶
Returns the profile of the logged-in admin account. Accessible to any admin (not just superadmin).
Auth: admin
Response — 200 OK¶
{
"id": 1,
"email": "admin@perfshop.fr",
"canAccessChaos": true,
"canAccessMonitoring": true,
"canAccessAdmin": true,
"canAccessJmeter": true,
"canAccessScripts": true,
"isSuperAdmin": true,
"createdAt": "2024-01-01T00:00:00Z"
}
The passwordHash field is never exposed in this DTO. Even under security chaos, only the hidden admin-portal can expose this field.
Admin account management¶
GET /api/admin/accounts¶
Lists all admin accounts.
Auth: superadmin only 👑
Response — 200 OK¶
Array of objects identical to the /accounts/me DTO:
[
{ "id": 1, "email": "admin@perfshop.fr", "isSuperAdmin": true, "canAccessChaos": true, "..." },
{ "id": 2, "email": "trainer@perfshop.fr", "isSuperAdmin": false, "canAccessChaos": true, "..." }
]
Error codes¶
| Code | Cause |
|---|---|
| 401 | Not authenticated |
| 403 | Ordinary admin (key admin.error.reserved_superadmin) |
POST /api/admin/accounts¶
Creates a new admin account.
Auth: superadmin 👑
Request¶
{
"email": "trainer@perfshop.fr",
"password": "Minimum6",
"canAccessChaos": true,
"canAccessMonitoring": true,
"canAccessAdmin": false,
"canAccessJmeter": false,
"canAccessScripts": false
}
| Field | Type | Required | Constraint |
|---|---|---|---|
email |
string | yes | Not empty, unique |
password |
string | yes | ≥ 6 characters |
canAccess* |
boolean | no (default false) |
Boolean |
Response — 201 Created¶
The created account serialized (same structure as GET /accounts/me).
Error codes¶
| Code | Cause |
|---|---|
| 400 | Missing email or password (admin.error.email_password_required_dot) |
| 400 | Password too short (admin.service.password_min_length) |
| 400 | Email already in use (admin.service.email_exists) |
| 401 | Not authenticated |
| 403 | Not superadmin |
DELETE /api/admin/accounts/{id}¶
Deletes an admin account.
Auth: superadmin 👑
Protection: the superadmin itself cannot be deleted (admin.service.superadmin_no_delete).
Response — 200 OK¶
Error codes¶
| Code | Cause |
|---|---|
| 401 | Not authenticated |
| 403 | Not superadmin, or attempting to delete the superadmin |
| 404 | Account not found |
PUT /api/admin/accounts/{id}/password¶
Changes the password of an admin account.
Auth:
- Superadmin: can change any account
- Ordinary admin: can only change their own password (check via id == me.id)
Request¶
Response — 200 OK¶
Error codes¶
| Code | Cause |
|---|---|
| 400 | Empty or too short password |
| 401 | Not authenticated |
| 403 | Attempting to change another account as ordinary admin (admin.error.own_password_only) |
| 404 | Account not found |
400 vs 404 detection
The AdminUserService.updatePassword() service throws IllegalArgumentException for two distinct reasons: password too short (→ 400) and account not found (→ 404). The controller distinguishes them by querying findById() separately: if the account exists, the password is the cause.
PUT /api/admin/accounts/{id}/rights¶
Modifies the access rights of an ordinary admin account.
Auth: superadmin 👑
Protection: the superadmin's rights are fixed and cannot be modified (admin.service.superadmin_rights_fixed).
Request¶
{
"canAccessChaos": true,
"canAccessMonitoring": true,
"canAccessAdmin": false,
"canAccessJmeter": false,
"canAccessScripts": false
}
Response — 200 OK¶
The updated account serialized.
Error codes¶
| Code | Cause |
|---|---|
| 401 | Not authenticated |
| 403 | Not superadmin, or attempting to modify the superadmin |
| 404 | Account not found |
Product management¶
GET /api/admin/products¶
Lists all non-pedagogical products (products with is_pedagogique = true are hidden).
Auth: admin Sorting: by category then by name
Response — 200 OK¶
[
{
"id": 1,
"name": "Bluetooth audio headset",
"description": "...",
"price": 149.99,
"stock": 42,
"category": "Audio",
"imageUrl": "/images/products/1.jpg",
"createdAt": "2025-01-15T10:00:00Z",
"updatedAt": "2025-03-20T14:30:00Z"
}
]
Pedagogical products hidden
Some products are created exclusively for the pedagogical journey (agents, narrative objects). They carry the is_pedagogique = true flag and are always filtered out of admin lists — they cannot be modified or deleted via /api/admin/products/*. An attempt returns 403 Forbidden with the message admin.error.pedagogique_protected.
POST /api/admin/products¶
Creates a product.
Auth: admin
Request¶
{
"name": "New product",
"description": "Detailed description",
"price": 29.99,
"stock": 100,
"category": "Computing",
"imageUrl": "/images/products/default.jpg"
}
All fields are mandatory except imageUrl.
Response — 201 Created¶
The created product serialized.
Error codes¶
| Code | Cause |
|---|---|
| 400 | Missing mandatory field or invalid format (admin.error.data_invalid) |
| 401 | Not authenticated |
PUT /api/admin/products/{id}¶
Updates an existing product. All body fields are optional — only those transmitted are updated.
Auth: admin
Request¶
Response — 200 OK¶
The updated product.
Error codes¶
| Code | Cause |
|---|---|
| 400 | Invalid data |
| 401 | Not authenticated |
| 403 | Protected pedagogical product (admin.error.pedagogique_protected) |
| 404 | Product not found |
DELETE /api/admin/products/{id}¶
Deletes a product.
Auth: admin
Response — 200 OK¶
Error codes¶
| Code | Cause |
|---|---|
| 401 | Not authenticated |
| 403 | Protected pedagogical product |
| 404 | Product not found |
POST /api/admin/products/{id}/image¶
Uploads an image for a product. The file is written to the mounted Docker volume and the URL is updated in the database.
Auth: admin
Content-Type: multipart/form-data
Expected field: file
Accepted formats: image/jpeg, image/png, image/webp, image/gif
Request¶
curl -H "X-Admin-Token: $TOKEN" \
-F "file=@/path/to/image.png" \
http://localhost:9080/api/admin/products/42/image
Response — 200 OK¶
The filename is normalized server-side: custom_<productId>.<ext>. The extension is chosen according to the received Content-Type.
Error codes¶
| Code | Cause |
|---|---|
| 400 | Content-Type does not start with image/ (admin.error.image_required) |
| 401 | Not authenticated |
| 404 | Product not found |
| 500 | Disk write failure (admin.error.file_write_failed) |
Where images are stored
The target directory is controlled by the Spring property perfshop.images.upload-path (default: /images-data/products). In Docker Compose, this path is a mounted volume that allows the nginx container to serve images via the /images/products/ URL prefix.
User management¶
GET /api/admin/users¶
Lists non-pedagogical end users with their order count.
Auth: admin
Response — 200 OK¶
[
{
"id": 42,
"email": "alice@example.com",
"firstName": "Alice",
"lastName": "Durand",
"createdAt": "2025-02-01T10:00:00Z",
"orderCount": 7
}
]
Accounts with is_pedagogique = true (narrative agents for the BAC5 journey) are filtered — they do not appear in this list and cannot be deleted via admin.
POST /api/admin/users¶
Creates an end user. Typically used for demos or to troubleshoot a student.
Auth: admin
Request¶
Response — 201 Created¶
The password is hashed with BCrypt via AuthService.hashPassword() before insertion.
Error codes¶
| Code | Cause |
|---|---|
| 400 | Missing email or password (admin.error.email_password_required) |
| 400 | Email already in use (admin.error.email_exists) |
| 401 | Not authenticated |
DELETE /api/admin/users/{id}¶
Deletes a user and all their orders (via orderRepository.deleteByUserId() upstream).
Auth: admin
Protection: accounts with is_pedagogique = true cannot be deleted (admin.error.user_pedagogique_protected).
Response — 200 OK¶
Error codes¶
| Code | Cause |
|---|---|
| 401 | Not authenticated |
| 403 | Protected pedagogical account |
| 404 | User not found |
Order management¶
GET /api/admin/orders¶
Lists all orders across all users.
Auth: admin
Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
includeTestData (query) |
boolean | false |
If true, includes orders with is_test_data = true from pedagogical agents |
Response — 200 OK¶
[
{
"id": 789,
"orderNumber": "PS-2026-000123",
"totalAmount": 299.98,
"status": "CONFIRMED",
"createdAt": "2026-04-08T10:30:00Z",
"shippingMethod": "express",
"shippingAddress": "12 rue de la Paix, 75001 Paris",
"userId": 42,
"userEmail": "alice@example.com",
"itemCount": 1,
"isTestData": false
}
]
Sorting: most recent first (createdAt DESC).
DELETE /api/admin/orders/{id}¶
Deletes an order.
Auth: admin
Response — 200 OK¶
Error codes¶
| Code | Cause |
|---|---|
| 401 | Not authenticated |
| 404 | Order not found |
Destructive deletion
This operation is permanent — it deletes the order and its order_items by JPA cascade. It does not re-credit stock. It is an administrative cleanup tool (e.g. purging test orders), not a replacement for POST /api/orders/{id}/cancel.
curl example — full admin workflow¶
# 1. Admin login
TOKEN=$(curl -s -H "Content-Type: application/json" \
-d '{"email":"admin@perfshop.fr","password":"admin"}' \
http://localhost:9080/api/admin/login | jq -r '.adminToken')
# 2. List products
curl -H "X-Admin-Token: $TOKEN" http://localhost:9080/api/admin/products
# 3. Create a product
curl -H "X-Admin-Token: $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Test","price":9.99,"stock":10,"category":"Misc"}' \
http://localhost:9080/api/admin/products
# 4. Upload image
curl -H "X-Admin-Token: $TOKEN" \
-F "file=@/tmp/logo.png" \
http://localhost:9080/api/admin/products/42/image
# 5. Create an ordinary admin account (superadmin required)
curl -H "X-Admin-Token: $TOKEN" \
-H "Content-Type: application/json" \
-d '{"email":"trainer@perfshop.fr","password":"trainer123","canAccessChaos":true}' \
http://localhost:9080/api/admin/accounts
Points of attention¶
This controller is NOT vulnerable
Unlike the hidden admin portal (admin-portal.md), AdminController is secure by construction:
- BCrypt for password comparison
- No SQLi (parameterized queries via JPA)
- No IDOR (systematic isSuperAdmin checks)
- No mass assignment (explicit DTOs)
Pedagogical faults are isolated in AdminPortalController which is only active at Security chaos level 4. This separation ensures that a misconfigured chaos never compromises the legitimate administration path.
No pagination
Admin lists (/products, /users, /orders, /accounts) are not paginated. This is acceptable because PerfShop is a pedagogical platform with limited data volumes (a few hundred rows max). In production with higher volumes, Spring Data pagination like in ProductController would need to be added.
Related links¶
admin-portal.md— hidden portal S10-S12 (Master level)chaos-admin.md— chaos control by the instructor- Admin authentication — details of the
AdminAuthhelper