Start learning free

LESSON 102 OF 173 · BACKEND: USERS AND SECURITY

Project: Reviews by Logged-in Users in Go

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 handler as 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:

// in newApp: app.HandleFunc("POST /wishlist", loginRequired(addToWishlist))

func addToWishlist(w http.ResponseWriter, r *http.Request, user User) {
	var data map[string]int
	json.NewDecoder(r.Body).Decode(&data)
	db.Exec("INSERT INTO wishlist (user_id, product_id) VALUES (?, ?)", user.ID, data["product_id"])
	writeJSON(w, 201, map[string]string{"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 handler: loginRequired answers 401. The body is decoded into a Review, which has only Stars and Text: a user_id in the body is simply dropped, and a missing stars leaves data.Stars at 0.

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 newApp. The route POST /products/{id}/reviews is already wrapped in loginRequired, and addReview already reads productID, decodes the body into data and answers 404 for a missing product. Add the stars check and the check for an earlier review by the same user, insert 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 the addReview handler, one check at a time.


Hint
The handler gets user from loginRequired, like getOrder. Do the checks in the order product, stars, earlier review, and only then insert. err already exists from the product lookup, so the second lookup uses =, not :=.


STEP 1 OF 11
    Ctrl + Enter to check

    Auth Reviews Project in Go

    Keep going with Go1,600+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free