Skip to content

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 upload
  • AdminAuth (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:

  1. POST /api/admin/login → stores admin_logged_in = true in the HttpSession
  2. Subsequent requests → Spring automatically transmits JSESSIONID
  3. AdminAuth.isAdmin() reads the session key

X-Admin-Token header:

  1. POST /api/admin/login → also returns a UUID adminToken in the body
  2. The token is stored in memory in AdminController.VALID_ADMIN_TOKENS (ConcurrentHashMap)
  3. Subsequent requests → the client sends X-Admin-Token: <uuid>
  4. 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 = true flag in admin_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

{
  "email": "admin@perfshop.fr",
  "password": "admin"
}

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

{ "success": true }

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

{ "authenticated": true }

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

{ "success": true }

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

{ "password": "NewStrongPassword" }

Response — 200 OK

{ "success": true }

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

{
  "price": 24.99,
  "stock": 75
}

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

{ "success": true }

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

{
  "success": true,
  "imageUrl": "/images/products/custom_42.png",
  "filename": "custom_42.png"
}

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

{
  "email": "new-user@example.com",
  "password": "AStrongPassword"
}

Response — 201 Created

{ "success": true, "id": 42 }

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

{ "success": true }

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

{ "success": true }

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.