API — Product catalog¶
This page documents ProductController, mounted under /api/products. All endpoints on this page are public (no authentication required).
Controller covered
ProductController → GET /, 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:
pricemultiplied by 1.5stockforced to 0descriptiontruncated
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 engineq=' 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¶
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"
Related links¶
- Admin product management — CRUD and image upload
- Security chaos S1 — SQL injection via
q - Functional chaos F4 — Silent product corruption
- Business chaos A13 — Wrong USD currency