diff --git a/backend/Makefile b/backend/Makefile index d93c067..c329b87 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -31,7 +31,7 @@ ifeq ($(RELEASE),1) OPTIMIZATION += -O3 else C_FLAGS += -g - F_FLAGS += -fsanitize=address,undefined + F_FLAGS += -fsanitize=address,undefined,leak OPTIMIZATION += -Og endif diff --git a/backend/src/controllers/carts.c b/backend/src/controllers/carts.c index 2fc60e0..f4b8fbf 100644 --- a/backend/src/controllers/carts.c +++ b/backend/src/controllers/carts.c @@ -71,4 +71,5 @@ void route_post_carts_purchase(HttpCtx* ctx) l0_return: receipt_destroy(&receipt); product_price_vec_destroy(&prices); + carts_purchase_req_destroy(&req); } diff --git a/backend/src/controllers/controllers.h b/backend/src/controllers/controllers.h index 1ef91a2..b0ea755 100644 --- a/backend/src/controllers/controllers.h +++ b/backend/src/controllers/controllers.h @@ -47,6 +47,7 @@ void route_post_sessions_logout(HttpCtx* ctx); void route_get_sessions_user(HttpCtx* ctx); void route_get_receipts_one(HttpCtx* ctx); +void route_get_receipts_all(HttpCtx* ctx); const Session* header_session(HttpCtx* ctx); const Session* middleware_session(HttpCtx* ctx); diff --git a/backend/src/controllers/receipts.c b/backend/src/controllers/receipts.c index bcb834b..58dc240 100644 --- a/backend/src/controllers/receipts.c +++ b/backend/src/controllers/receipts.c @@ -24,7 +24,8 @@ void route_get_receipts_one(HttpCtx* ctx) free(receipt_id_str); Receipt receipt; - DbRes db_res = db_receipt_with_id(cx->db, &receipt, receipt_id); + DbRes db_res = db_receipt_with_id_and_user_id( + cx->db, &receipt, receipt_id, session->user_id); if (db_res != DbRes_Ok) { RESPOND_BAD_REQUEST(ctx, "receipt not found"); return; @@ -48,7 +49,7 @@ void route_get_receipts_one(HttpCtx* ctx) ReceiptsOneRes res = { .receipt_id = receipt.id, - .timestamp = receipt.timestamp, + .timestamp = str_dup(receipt.timestamp), .products = (ReceiptsOneResProductVec) { 0 }, }; receipts_one_res_product_vec_construct(&res.products); @@ -56,7 +57,7 @@ void route_get_receipts_one(HttpCtx* ctx) receipts_one_res_product_vec_push(&res.products, (ReceiptsOneResProduct) { .product_id = products.data[i].id, - .name = products.data[i].name, + .name = str_dup(products.data[i].name), .price_dkk_cent = product_prices.data[i].price_dkk_cent, }); } @@ -64,9 +65,49 @@ void route_get_receipts_one(HttpCtx* ctx) char* res_json = receipts_one_res_to_json_string(&res); RESPOND_JSON(ctx, 200, "{\"ok\":true,\"receipt\":%s}", res_json); + free(res_json); receipts_one_res_destroy(&res); + for (size_t i = 0; i < products.size; ++i) + product_destroy(&products.data[i]); product_vec_destroy(&products); + for (size_t i = 0; i < products.size; ++i) + product_price_destroy(&product_prices.data[i]); product_price_vec_destroy(&product_prices); receipt_destroy(&receipt); } + +void route_get_receipts_all(HttpCtx* ctx) +{ + Cx* cx = http_ctx_user_ctx(ctx); + const Session* session = middleware_session(ctx); + if (!session) + return; + + ReceiptHeaderVec receipts; + receipt_header_vec_construct(&receipts); + DbRes db_res = db_receipt_all_headers_with_user_id( + cx->db, &receipts, session->user_id); + if (db_res != DbRes_Ok) { + RESPOND_SERVER_ERROR(ctx); + return; + } + + String receipts_str; + string_construct(&receipts_str); + for (size_t i = 0; i < receipts.size; ++i) { + if (i != 0) { + string_pushf(&receipts_str, ","); + } + char* receipt_json = receipt_header_to_json_string(&receipts.data[i]); + string_push_str(&receipts_str, receipt_json); + free(receipt_json); + } + + RESPOND_JSON( + ctx, 200, "{\"ok\":true,\"receipts\":[%s]}", receipts_str.data); + string_destroy(&receipts_str); + for (size_t i = 0; i < receipts.size; ++i) + receipt_header_destroy(&receipts.data[i]); + receipt_header_vec_destroy(&receipts); +} diff --git a/backend/src/db/db.h b/backend/src/db/db.h index 97efd59..78cd9a2 100644 --- a/backend/src/db/db.h +++ b/backend/src/db/db.h @@ -46,7 +46,12 @@ DbRes db_product_price_of_product( DbRes db_receipt_insert(Db* db, const Receipt* receipt, int64_t* id); /// `receipt` field is an out parameter. -DbRes db_receipt_with_id(Db* db, Receipt* receipt, int64_t id); +DbRes db_receipt_with_id_and_user_id( + Db* db, Receipt* receipt, int64_t id, int64_t user_id); + +/// Expects `receipts` to be constructed. +DbRes db_receipt_all_headers_with_user_id( + Db* db, ReceiptHeaderVec* receipts, int64_t user_id); /// `product_prices` field is an out parameter. /// Expects `product_prices` to be constructed. diff --git a/backend/src/db/db_sqlite.c b/backend/src/db/db_sqlite.c index 260dce7..e56058b 100644 --- a/backend/src/db/db_sqlite.c +++ b/backend/src/db/db_sqlite.c @@ -588,7 +588,8 @@ l0_return: return res; } -DbRes db_receipt_with_id(Db* db, Receipt* receipt, int64_t id) +DbRes db_receipt_with_id_and_user_id( + Db* db, Receipt* receipt, int64_t id, int64_t user_id) { static_assert(sizeof(Receipt) == 48, "model has changed"); @@ -598,7 +599,8 @@ DbRes db_receipt_with_id(Db* db, Receipt* receipt, int64_t id) sqlite3_stmt* stmt = NULL; int prepare_res = sqlite3_prepare_v2(connection, - "SELECT id, user, datetime(datetime) FROM receipts WHERE id = ?", + "SELECT id, user, datetime(datetime, 'unixepoch') FROM receipts" + " WHERE id = ? AND user = ?", -1, &stmt, NULL); @@ -610,6 +612,7 @@ DbRes db_receipt_with_id(Db* db, Receipt* receipt, int64_t id) } sqlite3_bind_int64(stmt, 1, id); + sqlite3_bind_int64(stmt, 2, user_id); int step_res = sqlite3_step(stmt); if (step_res == SQLITE_DONE) { @@ -668,6 +671,53 @@ l0_return: return res; } +DbRes db_receipt_all_headers_with_user_id( + Db* db, ReceiptHeaderVec* receipts, int64_t user_id) +{ + static_assert(sizeof(Receipt) == 48, "model has changed"); + + sqlite3* connection; + CONNECT; + DbRes res; + int sqlite_res; + + sqlite3_stmt* stmt; + sqlite_res = sqlite3_prepare_v2(connection, + "SELECT id, user, datetime(datetime, 'unixepoch') FROM receipts WHERE " + "user = ?", + -1, + &stmt, + NULL); + if (sqlite_res != SQLITE_OK) { + fprintf(stderr, "error: %s\n", sqlite3_errmsg(connection)); + res = DbRes_Error; + goto l0_return; + } + + sqlite3_bind_int64(stmt, 1, user_id); + + while ((sqlite_res = sqlite3_step(stmt)) == SQLITE_ROW) { + ReceiptHeader receipt = { + .id = GET_INT(0), + .user_id = GET_INT(1), + .timestamp = GET_STR(2), + }; + receipt_header_vec_push(receipts, receipt); + } + if (sqlite_res != SQLITE_DONE) { + fprintf(stderr, "error: %s\n", sqlite3_errmsg(connection)); + res = DbRes_Error; + goto l0_return; + } + + res = DbRes_Ok; +l0_return: + if (stmt) + sqlite3_finalize(stmt); + DISCONNECT; + return res; +} + DbRes db_receipt_prices( Db* db, ProductPriceVec* product_prices, int64_t receipt_id) { diff --git a/backend/src/main.c b/backend/src/main.c index 367de3c..dbb147f 100644 --- a/backend/src/main.c +++ b/backend/src/main.c @@ -42,6 +42,7 @@ int main(void) http_server_post(server, "/api/carts/purchase", route_post_carts_purchase); http_server_get(server, "/api/receipts/one", route_get_receipts_one); + http_server_get(server, "/api/receipts/all", route_get_receipts_all); http_server_post(server, "/api/users/register", route_post_users_register); http_server_post( diff --git a/backend/src/models/models.c b/backend/src/models/models.c index 0a00f7d..d3766a3 100644 --- a/backend/src/models/models.c +++ b/backend/src/models/models.c @@ -50,31 +50,41 @@ void receipt_destroy(Receipt* m) { static_assert(sizeof(Receipt) == 48, "model has changed"); - (void)m; + free(m->timestamp); + for (size_t i = 0; i < m->products.size; ++i) + receipt_product_destroy(&m->products.data[i]); + receipt_product_vec_destroy(&m->products); } -void users_register_req_destroy(UsersRegisterReq* model) +void receipt_header_destroy(ReceiptHeader* m) +{ + static_assert(sizeof(ReceiptHeader) == 24, "model has changed"); + + free(m->timestamp); +} + +void users_register_req_destroy(UsersRegisterReq* m) { static_assert(sizeof(UsersRegisterReq) == 24, "model has changed"); - free(model->name); - free(model->email); - free(model->password); + free(m->name); + free(m->email); + free(m->password); } -void sessions_login_req_destroy(SessionsLoginReq* model) +void sessions_login_req_destroy(SessionsLoginReq* m) { static_assert(sizeof(SessionsLoginReq) == 16, "model has changed"); - free(model->email); - free(model->password); + free(m->email); + free(m->password); } -void carts_purchase_req_destroy(CartsPurchaseReq* model) +void carts_purchase_req_destroy(CartsPurchaseReq* m) { static_assert(sizeof(CartsPurchaseReq) == 24, "model has changed"); - carts_item_vec_destroy(&model->items); + carts_item_vec_destroy(&m->items); } void receipts_one_res_product_destroy(ReceiptsOneResProduct* m) @@ -89,6 +99,8 @@ void receipts_one_res_destroy(ReceiptsOneRes* m) static_assert(sizeof(ReceiptsOneRes) == 40, "model has changed"); free(m->timestamp); + for (size_t i = 0; i < m->products.size; ++i) + receipts_one_res_product_destroy(&m->products.data[i]); receipts_one_res_product_vec_destroy(&m->products); } @@ -185,6 +197,7 @@ char* product_price_to_json_string(const ProductPrice* m) string_destroy(&string); return result; } + char* receipt_to_json_string(const Receipt* m) { static_assert(sizeof(Receipt) == 48, "model has changed"); @@ -223,6 +236,25 @@ char* receipt_to_json_string(const Receipt* m) return result; } +char* receipt_header_to_json_string(const ReceiptHeader* m) +{ + static_assert(sizeof(ReceiptHeader) == 24, "model has changed"); + + String string; + string_construct(&string); + string_pushf(&string, + "{" + "\"id\":%ld," + "\"user_id\":%ld," + "\"timestamp\":\"%s\"}", + m->id, + m->user_id, + m->timestamp); + char* result = string_copy(&string); + string_destroy(&string); + return result; +} + char* users_register_req_to_json_string(const UsersRegisterReq* m) { static_assert(sizeof(UsersRegisterReq) == 24, "model has changed"); @@ -311,9 +343,10 @@ char* receipts_one_res_to_json_string(const ReceiptsOneRes* m) if (i != 0) { string_pushf(&string, ","); } - char* product + char* product_json = receipts_one_res_product_to_json_string(&m->products.data[i]); - string_push_str(&string, product); + string_push_str(&string, product_json); + free(product_json); } string_pushf(&string, "]}"); @@ -446,6 +479,12 @@ int receipt_from_json(Receipt* m, const JsonValue* json) PANIC("not implemented"); } +int receipt_header_from_json(ReceiptHeader* m, const JsonValue* json) +{ + static_assert(sizeof(ReceiptHeader) == 24, "model has changed"); + + PANIC("not implemented"); +} int users_register_req_from_json(UsersRegisterReq* m, const JsonValue* json) { @@ -539,6 +578,8 @@ int receipts_one_res_from_json(ReceiptsOneRes* m, const JsonValue* json) DEFINE_VEC_IMPL(ProductPrice, ProductPriceVec, product_price_vec, ) DEFINE_VEC_IMPL(ReceiptProduct, ReceiptProductVec, receipt_product_vec, ) +DEFINE_VEC_IMPL(Receipt, ReceiptVec, receipt_vec, ) +DEFINE_VEC_IMPL(ReceiptHeader, ReceiptHeaderVec, receipt_header_vec, ) DEFINE_VEC_IMPL(CartsItem, CartsItemVec, carts_item_vec, ) DEFINE_VEC_IMPL(ReceiptsOneResProduct, ReceiptsOneResProductVec, diff --git a/backend/src/models/models.h b/backend/src/models/models.h index 6ecf7d5..27411ce 100644 --- a/backend/src/models/models.h +++ b/backend/src/models/models.h @@ -50,12 +50,23 @@ typedef struct { ReceiptProductVec products; } Receipt; +DECLARE_VEC_TYPE(Receipt, ReceiptVec, receipt_vec, ) + +typedef struct { + int64_t id; + int64_t user_id; + char* timestamp; +} ReceiptHeader; + +DECLARE_VEC_TYPE(ReceiptHeader, ReceiptHeaderVec, receipt_header_vec, ) + void user_destroy(User* model); void coord_destroy(Coord* model); void product_destroy(Product* model); void product_price_destroy(ProductPrice* model); void receipt_product_destroy(ReceiptProduct* model); void receipt_destroy(Receipt* model); +void receipt_header_destroy(ReceiptHeader* model); // diff --git a/backend/src/models/models_json.h b/backend/src/models/models_json.h index d64750a..73fc2cf 100644 --- a/backend/src/models/models_json.h +++ b/backend/src/models/models_json.h @@ -10,6 +10,7 @@ DEFINE_MODEL_JSON(Coord, coord) DEFINE_MODEL_JSON(Product, product) DEFINE_MODEL_JSON(ProductPrice, product_price) DEFINE_MODEL_JSON(Receipt, receipt) +DEFINE_MODEL_JSON(ReceiptHeader, receipt_header) DEFINE_MODEL_JSON(UsersRegisterReq, users_register_req) DEFINE_MODEL_JSON(SessionsLoginReq, sessions_login_req) diff --git a/backend/test/test.ts b/backend/test/test.ts index 45092d7..de907e9 100644 --- a/backend/test/test.ts +++ b/backend/test/test.ts @@ -105,6 +105,16 @@ async function testCartsAndReceipts(t: Deno.TestContext, token: string) { // console.log(res); assertEquals(res.ok, true); }); + + await t.step("test /api/receipts/all", async () => { + const res = await get<{ ok: boolean }>( + `/api/receipts/all`, + { "Session-Token": token }, + ); + + console.log(res); + assertEquals(res.ok, true); + }); } function get(