Skip to content

API — Product catalog

This page documents ProductController, mounted under /api/products. All endpoints on this page are public (no authentication required).

Controller covered

ProductControllerGET /, GET /{id}, GET /search, GET /categories

Admin endpoints (creation, modification, deletion, image upload) live on /api/admin/products/* — see admin.md.


Overview

Method Endpoint Auth Description
GET /api/products None Paginated list of all catalog products
GET /api/products/{id} None Product detail by identifier
GET /api/products/search None Multi-criteria search (text, category, price range)
GET /api/products/categories None List of distinct categories present in the database

GET /api/products

Returns the paginated list of products in the public catalog. Used by the frontend home page and the category pages.

Auth: none Controller: ProductController.getAllProducts() Service: ProductService.getAllProducts(Pageable)

Parameters

Parameter Type Default Description
page integer 0 Page number (0-indexed)
size integer 20 Number of products per page
sort string id Sort column — whitelist: id, name, price, category, createdAt, stock

Sort whitelist

A sort parameter not listed in the whitelist is silently replaced by id. This guardrail avoids a PropertyReferenceException that would propagate as a 500 if a client sent, for example, sort=password.

Response — 200 OK (nominal behavior)

{
  "content": [
    {
      "id": 1,
      "name": "Bluetooth audio headset",
      "description": "Wireless headset with active noise cancellation",
      "price": 149.99,
      "stock": 42,
      "category": "Audio",
      "imageUrl": "/images/products/1.jpg",
      "createdAt": "2025-01-15T10:00:00Z",
      "updatedAt": "2025-03-20T14:30:00Z",
      "pedagogique": false
    }
  ],
  "totalElements": 147,
  "totalPages": 8,
  "number": 0,
  "size": 20,
  "first": true,
  "last": false
}

A13 — Wrong display currency

In Business chaos level 4 (Master), the response is wrapped differently: a wrapper adds a currency: "USD" field while prices remain expressed in euros in the other endpoints. The structure becomes:

{
  "content": [ /* ... */ ],
  "totalElements": 147,
  "totalPages": 8,
  "number": 0,
  "size": 20,
  "currency": "USD"
}

No HTTP error — the symptom is only visible by inspecting the response. See A13.

Error codes

None under normal operation — this endpoint always returns 200 OK (empty list if the catalog is empty).


GET /api/products/{id}

Returns the detail of a product by its identifier.

Auth: none Controller: ProductController.getProductById()

Parameters

Parameter Type Description
id (path) Long Product identifier

Response — 200 OK

Returns the Product entity directly serialized:

{
  "id": 1,
  "name": "Bluetooth audio headset",
  "description": "Wireless headset with active noise cancellation",
  "price": 149.99,
  "stock": 42,
  "category": "Audio",
  "imageUrl": "/images/products/1.jpg",
  "createdAt": "2025-01-15T10:00:00Z",
  "updatedAt": "2025-03-20T14:30:00Z",
  "pedagogique": false
}

Error codes

Code Cause
404 Product not found (empty body)

F4 — Silent corruption

In Functional chaos level 4 (Master), this endpoint returns HTTP 200 with deliberately corrupted data:

  • price multiplied by 1.5
  • stock forced to 0
  • description truncated

No exception is raised, Tempo stays green, Grafana does not alert. Diagnosis requires manual payload inspection. This anomaly is particularly vicious because it illustrates that a system can be "down" without ever signaling it. See F4.


GET /api/products/search

Multi-criteria search on the catalog. Combines free text (q), a category filter and a price range.

Auth: none Controller: ProductController.searchProducts()

Parameters

Parameter Type Default Description
q string null Search text (name, description)
category string null Category filter (case-insensitive)
minPrice decimal null Minimum price (inclusive)
maxPrice decimal null Maximum price (inclusive)
page integer 0 Page number
size integer 20 Page size

Nominal behavior (Security chaos level 0)

Search uses ProductService.searchProducts() which executes a parameterized JPQL query — entirely immune to SQL injection. The q parameter is passed as a PreparedStatement to Hibernate.

Response — 200 OK

Structure identical to GET /api/products (Spring Data Page).

Error codes

None under normal operation — a search with no results returns 200 OK with content: [].

S1 — SQL Injection

In Security chaos level 1 (Junior) and above, the code path switches to ProductRepository.slowSearchFullScan(q) which executes a non-parameterized native SQL query using LIKE '%' || q || '%'. The q parameter is concatenated directly into the query without going through a PreparedStatement.

Exploitable payloads:

  • q=' OR '1'='1 → returns all products (filter bypass)
  • q='; SELECT password FROM users-- → error or data leak depending on the SQL engine
  • q=' UNION SELECT 1,email,password,1,1,1,1,1,1 FROM users-- → exfiltration

Any attempt is logged in SecurityChaosService's activityLog. See S1.


GET /api/products/categories

Returns the list of all categories present in the catalog.

Auth: none Controller: ProductController.getCategories() Service: ProductService.getAllCategories()SELECT DISTINCT category FROM products

Response — 200 OK

[
  "Audio",
  "Computing",
  "Peripherals",
  "Smartphone",
  "Tablet"
]

Returns a JSON array directly (no envelope). No pagination — the number of categories is always small (< 20).


curl example

# Full list (page 0, 20 products, sorted by price)
curl "http://localhost:9080/api/products?sort=price&size=20"

# Product detail
curl "http://localhost:9080/api/products/42"

# Search "headset" in Audio between 50 and 200 €
curl "http://localhost:9080/api/products/search?q=headset&category=Audio&minPrice=50&maxPrice=200"

# List categories
curl "http://localhost:9080/api/products/categories"