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 with the columns id (INTEGER PRIMARY KEY), product_id (INTEGER NOT NULL REFERENCES products(id)), user_id (INTEGER NOT NULL REFERENCES users(id)), stars (INTEGER NOT NULL) and text (TEXT NOT NULL).

The author comes from the login. The LoginRequired filter of the loggedIn group already finds the user from the token and saves it in context.Items["user"]. Save user["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:

loggedIn.MapPost("/wishlist", (JsonElement data, HttpContext context) => {
    var user = (Dictionary<string, object>)context.Items["user"];
    var cmd = new SqliteCommand("INSERT INTO wishlist (user_id, product_id) VALUES ($user_id, $product_id)", db);
    cmd.Parameters.AddWithValue("$user_id", user["id"]);
    cmd.Parameters.AddWithValue("$product_id", data.GetProperty("product_id").GetInt32());
    cmd.ExecuteNonQuery();
    return Results.Json(new { message = "Added" }, statusCode: 201);
});

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 route: the filter 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 and finish POST /products/{id}/reviews on the loggedIn group; the product lookup and its 404 are given. 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 build the reviews route, one check at a time.


Hint
cmd already holds $id from the product lookup, so reuse it: change its CommandText and add only the new parameters. Do the checks in the order product, stars, earlier review, and only then insert. Until the last return is written, C# reports error CS1643: not all code paths return a value.


STEP 1 OF 12
    Ctrl + Enter to check