Start learning free

LESSON 102 OF 173 · BACKEND: USERS AND SECURITY

Project: Reviews by Logged-in Users

Practice

This project puts the module together. Logged-in customers can review a product: a number of stars and a short text. Each review is a row in a new reviews table that keeps the product and the user who wrote it.

The author comes from the login. loginRequired already finds the user from the token and passes it to your method. Save user.get("id") as the author, never a user_id from the body: the client writes the body, so it could claim to be anyone. The same idea on another route:

@PostMapping("/wishlist")
Object addToWishlist(HttpServletRequest request, @RequestBody Map<String, Object> data) {
    return loginRequired(request, user -> saveWish(user, data));
}

Object saveWish(Map<String, Object> user, Map<String, Object> data) {
    db.update("INSERT INTO wishlist (user_id, product_id) VALUES (?, ?)", user.get("id"), data.get("product_id"));
    return ResponseEntity.status(201).body(Map.of("message", "Added"));
}

Before saving, check the request in this order: the product must exist (404 if not), the stars must be a whole number from 1 to 5 (400, naming the field), and the user must not have reviewed this product before (409 Conflict). A request without a login never reaches your method: loginRequired answers 401.

Ana signs up (she gets id 2) and logs in; the admin, already in the table, is user 1. Your route must answer:

POST /products/3/reviews   no token       {"stars": 5, "text": "Small but bright"}
   →  401  {"error": "Login required"}
POST /products/3/reviews   Ana's token    {"stars": 5, "text": "Small but bright", "user_id": 1}
   →  201  {"id": 1, "product_id": 3, "stars": 5, "text": "Small but bright", "user_id": 2}
POST /products/3/reviews   Ana's token    {"stars": 1, "text": "Changed my mind"}
   →  409  {"error": "You already reviewed this product"}
POST /products/1/reviews   Ana's token    {"stars": 6, "text": "Best mug ever"}
   →  400  {"error": "Stars must be a whole number from 1 to 5", "field": "stars"}
POST /products/1/reviews   Ana's token    {"stars": 0, "text": "Broke on day one"}
   →  400  {"error": "Stars must be a whole number from 1 to 5", "field": "stars"}
POST /products/99/reviews  Ana's token    {"stars": 4, "text": "Nice"}
   →  404  {"error": "Product not found"}
POST /products/3/reviews   admin's token  {"stars": 3, "text": "Too dim for reading"}
   →  201  {"id": 2, "product_id": 3, "stars": 3, "text": "Too dim for reading", "user_id": 1}

Your task: create the reviews table in the constructor. The route POST /products/{productId}/reviews is ready: it goes through loginRequired to saveReview(user, productId, data), which already checks the product. Finish saveReview: check the stars and an earlier review by the same user, save the review with the logged-in user as its author, and answer 201 with the new review.


Assignment
Follow the Coding Tutorial to create the reviews table and finish saveReview, one check at a time.


Hint
The stars check works like the price check in saveProduct: instanceof Integer stars, then the two ends. Do the checks in the order product, stars, earlier review, and only then insert the review with RETURNING id.


STEP 1 OF 9
    Ctrl + Enter to check