From b5f8c383c5c00a15aca89dcbcb999fffe0e22f98 Mon Sep 17 00:00:00 2001 From: Mikkel Troels Kongsted Date: Mon, 10 Mar 2025 12:00:22 +0100 Subject: [PATCH] login endpoint for backend --- backend/Makefile | 6 ++++ backend/prepare.sql | 2 +- backend/src/controllers.h | 10 +++++++ backend/src/controllers/auth.c | 44 +++++++++++++++++++++++++++++ backend/src/controllers/users.c | 10 +++---- backend/src/db.h | 7 +++++ backend/src/db_sqlite.c | 49 ++++++++++++++++++++++++++++++++- backend/src/main.c | 18 ++++++++++++ backend/src/models.c | 16 +++++------ backend/src/models.h | 16 +++++------ backend/src/str_util.c | 15 ++++++++-- backend/src/str_util.h | 2 ++ 12 files changed, 170 insertions(+), 25 deletions(-) diff --git a/backend/Makefile b/backend/Makefile index 108e0c0..f62cbd7 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -35,6 +35,12 @@ else OPTIMIZATION += -Og endif + +ifeq ($(RUN_TESTS),1) + C_FLAGS += -DRUN_TESTS +endif + + HEADERS = $(shell find src/ -name *.h) C_FILES = $(shell find src/ -name *.c) O_FILES = $(patsubst src/%.c,build/%.o,$(C_FILES)) diff --git a/backend/prepare.sql b/backend/prepare.sql index 063d479..8e62a3a 100644 --- a/backend/prepare.sql +++ b/backend/prepare.sql @@ -49,7 +49,7 @@ CREATE TABLE IF NOT EXISTS cart_items ( -INSERT OR REPLACE INTO users VALUES(1,'User','test@email.com','d980840fcb82970ab86656feebdccdd288be0e9b05f14e712b59529a2868fee3d980840fcb82970ab86656feebdccdd288be0e9b05f14e712b59529a2868fee3',10000); +INSERT OR REPLACE INTO users VALUES(1,'User','test@email.com','08ce0220f6d63d85c3ac313e308f4fca35ecfb850baa8ddb924cfab98137b6b18b4a8e027067cb98802757df1337246a0f3aa25c44c2b788517a871086419dcf',10000); INSERT OR REPLACE INTO products VALUES(1,'Letmælk',1195,'Mælk fra ko',NULL,NULL); INSERT OR REPLACE INTO products VALUES(2,'Smør',2000,'Smør fra mejeri',NULL,NULL); diff --git a/backend/src/controllers.h b/backend/src/controllers.h index 3a1d8b7..6d966d7 100644 --- a/backend/src/controllers.h +++ b/backend/src/controllers.h @@ -2,11 +2,13 @@ #include "db.h" #include "http_server.h" +#include "session.h" #include #include typedef struct { int number; + SessionVec sessions; Db* db; } Cx; @@ -18,6 +20,8 @@ void route_get_products_all(HttpCtx* ctx); void route_post_user_register(HttpCtx* ctx); +void route_post_auth_login(HttpCtx* ctx); + #define RESPOND(HTTP_CTX, STATUS, MIME_TYPE, ...) \ { \ HttpCtx* _ctx = (HTTP_CTX); \ @@ -42,3 +46,9 @@ void route_post_user_register(HttpCtx* ctx); RESPOND_JSON(HTTP_CTX, 400, "{\"ok\":false,\"msg\":\"%s\"}", (MSG)) #define RESPOND_SERVER_ERROR(HTTP_CTX) \ RESPOND_JSON(HTTP_CTX, 500, "{\"ok\":false,\"msg\":\"server error\"}") + +__attribute__((unused)) +static inline void ___include_user(void) +{ + RESPOND((HttpCtx*)0, 200, "text/html", "") +} diff --git a/backend/src/controllers/auth.c b/backend/src/controllers/auth.c index 754ee6a..08f2a4d 100644 --- a/backend/src/controllers/auth.c +++ b/backend/src/controllers/auth.c @@ -2,9 +2,53 @@ #include "../http_server.h" #include "../models_json.h" #include "../str_util.h" +#include void route_post_auth_login(HttpCtx* ctx) { Cx* cx = http_ctx_user_ctx(ctx); + + const char* body_str = http_ctx_req_body(ctx); + + JsonValue* body_json = json_parse(body_str, strlen(body_str)); + + AuthLoginReq req; + int parse_res = auth_login_req_from_json(&req, body_json); + json_free(body_json); + + if (parse_res != 0) { + RESPOND_BAD_REQUEST(ctx, "bad request"); + goto l0_return; + } + if (strlen(req.email) == 0 + || strlen(req.password) > MAX_HASH_INPUT_LEN) { + + RESPOND_BAD_REQUEST(ctx, "bad request"); + goto l0_return; + } + + User user; + DbRes db_res = db_user_from_email(cx->db, &user, req.email); + if (db_res == DbRes_NotFound) { + RESPOND_BAD_REQUEST(ctx, "user with email not found"); + goto l0_return; + } + else if (db_res == DbRes_Error) { + RESPOND_SERVER_ERROR(ctx); + goto l0_return; + } + + if (!str_hash_equal(user.password_hash, req.password)) { + RESPOND_BAD_REQUEST(ctx, "wrong password"); + goto l2_return; + } + + session_vec_push(&cx->sessions, (Session) {.user_id = user.id}); + + RESPOND_JSON(ctx, 200, "{\"ok\":true}"); +l2_return: + user_destroy(&user); +l0_return: + auth_login_req_destroy(&req); } diff --git a/backend/src/controllers/users.c b/backend/src/controllers/users.c index 910a476..9cb20f7 100644 --- a/backend/src/controllers/users.c +++ b/backend/src/controllers/users.c @@ -23,7 +23,7 @@ void route_post_user_register(HttpCtx* ctx) if (strlen(req.name) == 0 || strlen(req.email) == 0 || strlen(req.password) > MAX_HASH_INPUT_LEN) { RESPOND_BAD_REQUEST(ctx, "bad request"); - users_register_req_free(&req); + users_register_req_destroy(&req); return; } @@ -32,13 +32,13 @@ void route_post_user_register(HttpCtx* ctx) if (db_users_with_email(cx->db, &ids, req.email) != DbRes_Ok) { RESPOND_SERVER_ERROR(ctx); ids_destroy(&ids); - users_register_req_free(&req); + users_register_req_destroy(&req); return; } if (ids.size > 0) { RESPOND_BAD_REQUEST(ctx, "email in use"); ids_destroy(&ids); - users_register_req_free(&req); + users_register_req_destroy(&req); return; } ids_destroy(&ids); @@ -57,10 +57,10 @@ void route_post_user_register(HttpCtx* ctx) RESPOND_SERVER_ERROR(ctx); free(password_hash); - users_register_req_free(&req); + users_register_req_destroy(&req); return; } free(password_hash); - users_register_req_free(&req); + users_register_req_destroy(&req); RESPOND_JSON(ctx, 200, "{\"ok\":true}"); } diff --git a/backend/src/db.h b/backend/src/db.h index dfba81e..63ef467 100644 --- a/backend/src/db.h +++ b/backend/src/db.h @@ -16,9 +16,16 @@ typedef struct Db Db; /// `user.id` field is ignored. DbRes db_user_insert(Db* db, const User* user); +/// `user` field is an out parameter. DbRes db_user_from_id(Db* db, User* user, int64_t id); + + /// Expects `ids` to be constructed. DbRes db_users_with_email(Db* db, Ids* ids, const char* email); + +/// `user` is an out parameter. +DbRes db_user_from_email(Db* db, User* user, const char* email); + /// Expects `vec` to be constructed. DbRes db_product_all(Db* db, ProductVec* vec); diff --git a/backend/src/db_sqlite.c b/backend/src/db_sqlite.c index 7ef6c55..dd85b4b 100644 --- a/backend/src/db_sqlite.c +++ b/backend/src/db_sqlite.c @@ -110,7 +110,7 @@ DbRes db_user_from_id(Db* db, User* user, int64_t id) sqlite3_stmt* stmt; sqlite3_prepare_v2(connection, "SELECT id, name, email, password_hash, balance_dkk_cent" - "FROM users WHERE id = ?", + " FROM users WHERE id = ?", -1, &stmt, NULL); sqlite3_bind_int64(stmt, 1, id); @@ -171,6 +171,53 @@ l0_return: return res; } +DbRes db_user_from_email(Db* db, User* user, const char* email) +{ + static_assert(sizeof(User) == 40, "model has changed"); + + sqlite3* connection; + CONNECT; + DbRes res; + + sqlite3_stmt* stmt; + int prepare_res = sqlite3_prepare_v2(connection, + "SELECT id, name, email, password_hash, balance_dkk_cent" + " FROM users WHERE email = ?", + -1, &stmt, NULL); + if (prepare_res != SQLITE_OK) { + fprintf(stderr, "error: %s\n at %s:%d\n", sqlite3_errmsg(connection), __func__, __LINE__); + res = DbRes_Error; + goto l0_return; + } + sqlite3_bind_text(stmt, 1, email, -1, NULL); + + int step_res = sqlite3_step(stmt); + if (step_res == SQLITE_DONE) { + res = DbRes_NotFound; + goto l0_return; + } else if (step_res != SQLITE_ROW) { + printf("step_res = %d, email = '%s'\n", step_res, email); + fprintf(stderr, "error: %s\n at %s:%d\n", sqlite3_errmsg(connection), __func__, __LINE__); + res = DbRes_Error; + goto l0_return; + } + *user = (User) { + .id = GET_INT(0), + .name = GET_STR(1), + .email = GET_STR(2), + .password_hash = GET_STR(3), + .balance_dkk_cent = GET_INT(4), + }; + + res = DbRes_Ok; +l0_return: + if (stmt) + sqlite3_finalize(stmt); + DISCONNECT; + return res; +} + + DbRes db_product_all(Db* db, ProductVec* vec) { sqlite3* connection; diff --git a/backend/src/main.c b/backend/src/main.c index cad978a..43fa331 100644 --- a/backend/src/main.c +++ b/backend/src/main.c @@ -4,20 +4,30 @@ #include "json.h" #include "models.h" #include "models_json.h" +#include "session.h" #include "str_util.h" #include #include #include + +void test(void); + + HttpServer* server; int main(void) { + #ifdef RUN_TESTS + test(); + #endif + Db* db = db_sqlite_new(); Cx cx = { .number = 1, .db = db, }; + session_vec_construct(&cx.sessions); server = http_server_new((HttpServerOpts) { .port = 8080, @@ -32,6 +42,7 @@ int main(void) http_server_get(server, "/api/products/all", route_get_products_all); http_server_post(server, "/api/users/register", route_post_user_register); + http_server_post(server, "/api/auth/login", route_post_auth_login); http_server_get(server, "/", route_get_index); http_server_post(server, "/set_number", route_post_set_number); @@ -44,3 +55,10 @@ int main(void) http_server_free(server); db_sqlite_free(db); } + +void test(void) +{ + str_util_test(); + printf("ALL TESTS PASSED 💅\n"); + exit(0); +} diff --git a/backend/src/models.c b/backend/src/models.c index 81e8c6f..148959c 100644 --- a/backend/src/models.c +++ b/backend/src/models.c @@ -6,7 +6,7 @@ #include #include -void user_free(User* m) +void user_destroy(User* m) { static_assert(sizeof(User) == 40, "model has changed"); @@ -15,14 +15,14 @@ void user_free(User* m) free(m->password_hash); } -void coord_free(Coord* m) +void coord_destroy(Coord* m) { static_assert(sizeof(Coord) == 24, "model has changed"); (void)m; } -void product_free(Product* m) +void product_destroy(Product* m) { static_assert(sizeof(Product) == 40, "model has changed"); @@ -30,28 +30,28 @@ void product_free(Product* m) free(m->barcode); } -void product_price_free(ProductPrice* m) +void product_price_destroy(ProductPrice* m) { static_assert(sizeof(ProductPrice) == 24, "model has changed"); (void)m; } -void cart_free(Cart* m) +void cart_destroy(Cart* m) { static_assert(sizeof(Cart) == 16, "model has changed"); (void)m; } -void cart_item_free(CartItem* m) +void cart_item_destroy(CartItem* m) { static_assert(sizeof(CartItem) == 24, "model has changed"); (void)m; } -void users_register_req_free(UsersRegisterReq* model) +void users_register_req_destroy(UsersRegisterReq* model) { static_assert(sizeof(UsersRegisterReq) == 24, "model has changed"); @@ -60,7 +60,7 @@ void users_register_req_free(UsersRegisterReq* model) free(model->password); } -void auth_login_req_free(AuthLoginReq* model) +void auth_login_req_destroy(AuthLoginReq* model) { static_assert(sizeof(AuthLoginReq) == 16, "model has changed"); diff --git a/backend/src/models.h b/backend/src/models.h index fec6cfe..e75df51 100644 --- a/backend/src/models.h +++ b/backend/src/models.h @@ -41,12 +41,12 @@ typedef struct { int64_t amount; } CartItem; -void user_free(User* model); -void coord_free(Coord* model); -void product_free(Product* model); -void product_price_free(ProductPrice* model); -void cart_free(Cart* model); -void cart_item_free(CartItem* model); +void user_destroy(User* model); +void coord_destroy(Coord* model); +void product_destroy(Product* model); +void product_price_destroy(ProductPrice* model); +void cart_destroy(Cart* model); +void cart_item_destroy(CartItem* model); // @@ -61,5 +61,5 @@ typedef struct { char* password; } AuthLoginReq; -void users_register_req_free(UsersRegisterReq* model); -void auth_login_req_free(AuthLoginReq* model); +void users_register_req_destroy(UsersRegisterReq* model); +void auth_login_req_destroy(AuthLoginReq* model); diff --git a/backend/src/str_util.c b/backend/src/str_util.c index 6894ea6..2727b2d 100644 --- a/backend/src/str_util.c +++ b/backend/src/str_util.c @@ -1,4 +1,5 @@ #include "str_util.h" +#include "panic.h" #include #include #include @@ -124,7 +125,7 @@ static inline char* hashdata_to_string(HashData hash) } for (size_t i = 0; i < STR_HASH_HASH_SIZE; ++i) { char bytestr[3] = { 0 }; - snprintf(bytestr, 3, "%02x", hash.salt[i]); + snprintf(bytestr, 3, "%02x", hash.hash[i]); result[(STR_HASH_SALT_SIZE + i) * 2] = bytestr[0]; result[(STR_HASH_SALT_SIZE + i) * 2 + 1] = bytestr[1]; } @@ -144,7 +145,6 @@ static inline HashData hashdata_from_hash_string(const char* str) } HashData hash; - // memcpy((uint8_t*)&hash, result, sizeof(result)); for (size_t i = 0; i < 32; ++i) { hash.salt[i] = result[i]; hash.hash[i] = result[32 + i]; @@ -163,3 +163,14 @@ bool str_hash_equal(const char* hash, const char* input) HashData data = hashdata_from_hash_string(hash); return hashdata_is_equal(data, input); } + +void str_util_test(void) { + char* hash = str_hash("1234"); + if (!str_hash_equal(hash, "1234")) { + PANIC("hash should be equal"); + } + if (str_hash_equal(hash, "4321")) { + PANIC("hash should not be equal"); + } + free(hash); +} diff --git a/backend/src/str_util.h b/backend/src/str_util.h index 0ebe580..44bebfc 100644 --- a/backend/src/str_util.h +++ b/backend/src/str_util.h @@ -37,3 +37,5 @@ DEFINE_VEC(char*, RawStrVec, rawstr_vec, 8) char* str_hash(const char* input); bool str_hash_equal(const char* hash, const char* input); + +void str_util_test(void);