An AI assistant wrote the shop's sign-up route. It answers 201 and looks fine, but look at what it saves:
app.MapPost("/signup", (JsonElement data) => {
var cmd = db.CreateCommand();
cmd.CommandText = "INSERT INTO users (email, password_hash) VALUES ($email, $password_hash); SELECT last_insert_rowid()";
cmd.Parameters.AddWithValue("$email", data.GetProperty("email").GetString());
cmd.Parameters.AddWithValue("$password_hash", data.GetProperty("password").GetString());
var id = cmd.ExecuteScalar();
return Results.Json(new { id = id, email = data.GetProperty("email").GetString() }, statusCode: 201);
});
The column is called password_hash, but it holds the password itself. Anyone who sees the database (a backup, a leak) reads every password, and people reuse passwords on other sites. It also breaks the login: the shop's /login route, already written for you, checks passwords with CheckPasswordHash, which expects a hash. With the AI's sign-up, every login answers 401.
A hash is a one-way scramble: the password gives the hash, but the hash never gives the password back. The file already has two helpers above the routes, built on .NET's PBKDF2. GeneratePasswordHash mixes in a random salt first, so two people with the same password get different hashes. CheckPasswordHash tells you whether a password fits a saved hash:
var stored = GeneratePasswordHash("open sesame");
Console.WriteLine(stored.Substring(0, 21)); // pbkdf2:sha256:600000$
Console.WriteLine(CheckPasswordHash(stored, "open sesame")); // True
Console.WriteLine(CheckPasswordHash(stored, "Open sesame")); // False
The requests and the answers your app must give. GET /check/users is a checking route the course adds: it shows the users table with each salt and hash replaced by a label like <salt 1>.
POST /signup {"email": "ana@example.com", "password": "secret123"} → 201 {"email": "ana@example.com", "id": 1}
POST /signup {"email": "ben@example.com", "password": "secret123"} → 201 {"email": "ben@example.com", "id": 2}
GET /check/users → 200 [{"email": "ana@example.com", "id": 1, "password_hash": "pbkdf2:sha256:600000$<salt 1>$<hash 1>"},
{"email": "ben@example.com", "id": 2, "password_hash": "pbkdf2:sha256:600000$<salt 2>$<hash 2>"}]
POST /login {"email": "ana@example.com", "password": "secret123"} → 200 {"email": "ana@example.com", "id": 1}
POST /login {"email": "ana@example.com", "password": "Secret123"} → 401 {"error": "Wrong email or password"}
POST /login {"email": "ben@example.com", "password": "secret123"} → 200 {"email": "ben@example.com", "id": 2}
Your task: rewrite the body of the /signup route. Save the email and GeneratePasswordHash of the password, never the password itself, and answer 201 with the new user's id and email.
Assignment
Follow the Coding Tutorial to rewrite the /signup route so it stores a salted hash instead of the password.
Hint
The /login route is already correct: it only needs a real hash in the column. GeneratePasswordHash is already in the file, above the routes. Keep both the password and the hash out of the answer.
Free plan: 30 tutor messages. PRO is unlimited.
