From a7591a96ff9e524c9cd5727539e078fd0873225e Mon Sep 17 00:00:00 2001 From: SimonFJ20 Date: Wed, 19 Mar 2025 17:26:36 +0100 Subject: [PATCH] product images --- backend/prepare.sql | 8 + backend/public/deno.jsonc | 9 ++ backend/public/product_editor.html | 122 ++++++++++----- backend/public/product_editor.js | 79 +++++++--- backend/public/product_fallback_256x256.png | Bin 0 -> 3724 bytes backend/public/product_fallback_512x512.png | Bin 0 -> 6437 bytes backend/src/controllers/carts.c | 2 +- backend/src/controllers/controllers.h | 13 +- backend/src/controllers/general.c | 2 +- backend/src/controllers/products.c | 161 +++++++++++++++++--- backend/src/controllers/receipts.c | 4 + backend/src/controllers/sessions.c | 2 +- backend/src/controllers/users.c | 2 +- backend/src/db/db.h | 7 + backend/src/db/db_sqlite.c | 85 +++++++++++ backend/src/http/http.h | 8 +- backend/src/http/server.c | 34 ++++- backend/src/http/server.h | 3 +- backend/src/http/worker.c | 3 +- backend/src/main.c | 4 + 20 files changed, 451 insertions(+), 97 deletions(-) create mode 100644 backend/public/deno.jsonc create mode 100644 backend/public/product_fallback_256x256.png create mode 100644 backend/public/product_fallback_512x512.png diff --git a/backend/prepare.sql b/backend/prepare.sql index de14924..8592e0a 100644 --- a/backend/prepare.sql +++ b/backend/prepare.sql @@ -51,6 +51,14 @@ CREATE TABLE IF NOT EXISTS receipt_products ( FOREIGN KEY(product_price) REFERENCES product_prices(id) ); +CREATE TABLE IF NOT EXISTS product_images ( + id INTEGER PRIMARY KEY, + product INTEGER NOT NULL UNIQUE, + data BLOB NOT NULL, + + FOREIGN KEY(product) REFERENCES products(id) +); + INSERT OR REPLACE INTO users (name, email, password_hash, balance_dkk_cent) VALUES ('User','test@email.com','08ce0220f6d63d85c3ac313e308f4fca35ecfb850baa8ddb924cfab98137b6b18b4a8e027067cb98802757df1337246a0f3aa25c44c2b788517a871086419dcf',10000); diff --git a/backend/public/deno.jsonc b/backend/public/deno.jsonc new file mode 100644 index 0000000..4d62fa6 --- /dev/null +++ b/backend/public/deno.jsonc @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "checkJs": false, + "lib": ["dom", "dom.iterable", "dom.asynciterable", "deno.ns"] + }, + "fmt": { + "indentWidth": 4 + } +} diff --git a/backend/public/product_editor.html b/backend/public/product_editor.html index 3db5a9e..b5858a6 100644 --- a/backend/public/product_editor.html +++ b/backend/public/product_editor.html @@ -59,51 +59,97 @@ #editor input, #editor textarea { width: 250px; } + #wrapper { + display: flex; + justify-content: center; + gap: 2rem; + } + #wrapper form { + min-width: 500px; + } + #image-uploader { + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + height: 100%; + } + #image-uploader #preview { + height: 150px; + background-color: #fff; + }

Products

-
- Editor +
+
+ Editor -
-
- + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ + + +
+ +
+
+ Image uploader + +
+ + + + + + + + + +
+ +
+ +
+
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
-
-
+ +
+ diff --git a/backend/public/product_editor.js b/backend/public/product_editor.js index b130570..f295dc5 100644 --- a/backend/public/product_editor.js +++ b/backend/public/product_editor.js @@ -1,16 +1,22 @@ - const productList = document.querySelector("#product-list"); const editor = { form: document.querySelector("#editor"), loadButton: document.querySelector("#editor #load"), saveButton: document.querySelector("#editor #save"), - newButton: document.querySelector("#editor #new"), - idInput: document.querySelector("#editor #product-id"), - nameInput: document.querySelector("#editor #product-name"), - priceInput: document.querySelector("#editor #product-price"), - descriptionTextarea: document.querySelector("#editor #product-description"), - coordInput: document.querySelector("#editor #product-coord"), + newButton: document.querySelector("#editor #new"), + idInput: document.querySelector("#editor #product-id"), + nameInput: document.querySelector("#editor #product-name"), + priceInput: document.querySelector("#editor #product-price"), + coordInput: document.querySelector("#editor #product-coord"), barcodeInput: document.querySelector("#editor #product-barcode"), + descriptionTextarea: document.querySelector("#editor #product-description"), +}; +const imageUploader = { + form: document.querySelector("#image-uploader"), + idInput: document.querySelector("#image-uploader #product-id"), + saveButton: document.querySelector("#image-uploader #save"), + preview: document.querySelector("#image-uploader #preview"), + fileInput: document.querySelector("#image-uploader #file"), }; let products = []; @@ -28,11 +34,13 @@ function selectProduct(product) { editor.barcodeInput.value = product.barcode.toString(); } -async function loadProduct() { +function loadProduct() { selectedProductId = parseInt(editor.idInput.value); - const product = products.find(product => product.id === selectedProductId); - if (!product){ + const product = products.find((product) => + product.id === selectedProductId + ); + if (!product) { alert(`no product with id ${selectedProductId}`); return; } @@ -48,35 +56,34 @@ function productFromForm() { price_dkk_cent: Math.floor(parseFloat(editor.priceInput.value) * 100), coord_id: parseInt(editor.coordInput.value), barcode: editor.barcodeInput.value, - } + }; } async function saveProduct() { const product = productFromForm(); await fetch("/api/products/update", { method: "POST", - headers: {"Content-Type": "application/json"}, + headers: { "Content-Type": "application/json" }, body: JSON.stringify(product), - }).then(res => res.json()); + }).then((res) => res.json()); await updateProductList(); - } async function newProduct() { const product = productFromForm(); await fetch("/api/products/create", { method: "POST", - headers: {"Content-Type": "application/json"}, + headers: { "Content-Type": "application/json" }, body: JSON.stringify(product), - }).then(res => res.json()); + }).then((res) => res.json()); await updateProductList(); } async function updateProductList() { const res = await fetch("/api/products/all") - .then(res => res.json()); + .then((res) => res.json()); products = res.products; @@ -92,7 +99,7 @@ async function updateProductList() { `; productList.innerHTML += products - .map(product => ` + .map((product) => ` ${product.id} ${product.name} @@ -121,15 +128,45 @@ editor.form e.preventDefault(); }); editor.loadButton - .addEventListener("click", (e) => { + .addEventListener("click", (_e) => { loadProduct(); }); editor.saveButton - .addEventListener("click", (e) => { + .addEventListener("click", (_e) => { saveProduct(); }); editor.newButton - .addEventListener("click", (e) => { + .addEventListener("click", (_e) => { newProduct(); }); +imageUploader.form + .addEventListener("submit", (e) => { + e.preventDefault(); + }); +imageUploader.fileInput + .addEventListener("input", (e) => { + console.log(e); + const image = imageUploader.fileInput.files[0]; + const data = URL.createObjectURL(image); + imageUploader.preview.src = data; + }); +imageUploader.saveButton + .addEventListener("click", async (_e) => { + const id = parseInt(imageUploader.idInput.value); + const image = imageUploader.fileInput.files[0]; + + const buffer = await new Promise((resolve) => { + const reader = new FileReader(); + reader.addEventListener("loadend", () => { + resolve(reader.result); + }); + reader.readAsArrayBuffer(image); + }); + + await fetch(`/api/products/set-image?product_id=${id}`, { + method: "post", + headers: { "Content-Type": image.type }, + body: buffer, + }).then((res) => res.json()); + }); diff --git a/backend/public/product_fallback_256x256.png b/backend/public/product_fallback_256x256.png new file mode 100644 index 0000000000000000000000000000000000000000..568f4f7db861e1ea2d9d1458603e2a5d59ea136e GIT binary patch literal 3724 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

400 Bad " \ + "Request

%s

", \ + __VA_ARGS__); + #define RESPOND_HTML_SERVER_ERROR(CTX) \ RESPOND_HTML(CTX, \ 500, \ diff --git a/backend/src/controllers/general.c b/backend/src/controllers/general.c index 0d33d70..8485aef 100644 --- a/backend/src/controllers/general.c +++ b/backend/src/controllers/general.c @@ -18,7 +18,7 @@ void route_post_set_number(HttpCtx* ctx) { Cx* cx = http_ctx_user_ctx(ctx); - const char* body_text = http_ctx_req_body(ctx); + const char* body_text = http_ctx_req_body_str(ctx); JsonParser parser; json_parser_construct(&parser, body_text, strlen(body_text)); JsonValue* body = json_parser_parse(&parser); diff --git a/backend/src/controllers/products.c b/backend/src/controllers/products.c index cbb356a..d9318d3 100644 --- a/backend/src/controllers/products.c +++ b/backend/src/controllers/products.c @@ -40,7 +40,7 @@ void route_post_products_create(HttpCtx* ctx) { Cx* cx = http_ctx_user_ctx(ctx); - const char* body_str = http_ctx_req_body(ctx); + const char* body_str = http_ctx_req_body_str(ctx); JsonValue* body_json = json_parse(body_str, strlen(body_str)); if (!body_json) { RESPOND_BAD_REQUEST(ctx, "bad request"); @@ -81,7 +81,7 @@ void route_post_products_update(HttpCtx* ctx) { Cx* cx = http_ctx_user_ctx(ctx); - const char* body_str = http_ctx_req_body(ctx); + const char* body_str = http_ctx_req_body_str(ctx); printf("body_str = '%s'\n", body_str); JsonValue* body_json = json_parse(body_str, strlen(body_str)); @@ -113,10 +113,126 @@ l0_return: product_destroy(&product); } -static inline int read_and_send_file(HttpCtx* ctx, - const char* filepath, - size_t max_file_size, - const char* mime_type) +void route_post_products_set_image(HttpCtx* ctx) +{ + Cx* cx = http_ctx_user_ctx(ctx); + + const char* query = http_ctx_req_query(ctx); + if (!query) { + RESPOND_BAD_REQUEST(ctx, "no product_id parameter"); + return; + } + HttpQueryParams* params = http_parse_query_params(query); + char* product_id_str = http_query_params_get(params, "product_id"); + http_query_params_free(params); + if (!product_id_str) { + RESPOND_BAD_REQUEST(ctx, "no product_id parameter"); + return; + } + + int64_t product_id = strtol(product_id_str, NULL, 10); + free(product_id_str); + + const uint8_t* body = http_ctx_req_body(ctx); + size_t body_size = http_ctx_req_body_size(ctx); + + DbRes db_res = db_product_image_insert(cx->db, product_id, body, body_size); + if (db_res != DbRes_Ok) { + RESPOND_SERVER_ERROR(ctx); + return; + } + + RESPOND_JSON(ctx, 200, "{\"ok\":true}"); +} + +static inline int read_fallback_image(uint8_t** buffer, size_t* buffer_size) +{ + int res; + + const char* filepath = PUBLIC_DIR_PATH "/product_fallback_256x256.png"; + + FILE* fp = fopen(filepath, "r"); + if (!fp) { + return -1; + } + fseek(fp, 0L, SEEK_END); + size_t file_size = (size_t)ftell(fp); + rewind(fp); + + const size_t max_file_size = 16777216; + if (file_size >= max_file_size) { + fprintf(stderr, + "error: file too large '%s' >= %ld\n", + filepath, + max_file_size); + res = -1; + goto l0_return; + } + + uint8_t* temp_buffer = malloc(file_size); + size_t bytes_read = fread(temp_buffer, sizeof(char), file_size, fp); + if (bytes_read != file_size) { + fprintf(stderr, "error: could not read file '%s'\n", filepath); + res = -1; + goto l1_return; + } + + *buffer = temp_buffer; + *buffer_size = file_size; + temp_buffer = NULL; + + res = 0; +l1_return: + if (temp_buffer) + free(temp_buffer); +l0_return: + fclose(fp); + return res; +} + +void route_get_products_image_png(HttpCtx* ctx) +{ + Cx* cx = http_ctx_user_ctx(ctx); + + const char* query = http_ctx_req_query(ctx); + if (!query) { + RESPOND_HTML_BAD_REQUEST(ctx, "no product_id parameter"); + return; + } + HttpQueryParams* params = http_parse_query_params(query); + char* product_id_str = http_query_params_get(params, "product_id"); + http_query_params_free(params); + if (!product_id_str) { + RESPOND_HTML_BAD_REQUEST(ctx, "no product_id parameter"); + return; + } + + int64_t product_id = strtol(product_id_str, NULL, 10); + free(product_id_str); + + uint8_t* buffer; + size_t buffer_size; + + DbRes db_res = db_product_image_with_product_id( + cx->db, &buffer, &buffer_size, product_id); + if (db_res == DbRes_NotFound) { + int res = read_fallback_image(&buffer, &buffer_size); + if (res != 0) { + RESPOND_HTML_SERVER_ERROR(ctx); + return; + } + } else if (db_res != DbRes_Ok) { + RESPOND_HTML_SERVER_ERROR(ctx); + return; + } + + http_ctx_res_headers_set(ctx, "Content-Type", "image/png"); + + http_ctx_respond(ctx, 200, buffer, buffer_size); +} + +static inline int read_and_send_file( + HttpCtx* ctx, const char* filepath, const char* mime_type) { int res; @@ -125,14 +241,12 @@ static inline int read_and_send_file(HttpCtx* ctx, RESPOND_HTML_SERVER_ERROR(ctx); return -1; } + fseek(fp, 0L, SEEK_END); + size_t file_size = (size_t)ftell(fp); + rewind(fp); - char* buf = calloc(max_file_size + 1, sizeof(char)); - size_t bytes_read = fread(buf, sizeof(char), max_file_size, fp); - if (bytes_read == 0) { - RESPOND_HTML_SERVER_ERROR(ctx); - res = -1; - goto l0_return; - } else if (bytes_read >= max_file_size) { + const size_t max_file_size = 16777216; + if (file_size >= max_file_size) { fprintf(stderr, "error: file too large '%s' >= %ld\n", filepath, @@ -142,13 +256,18 @@ static inline int read_and_send_file(HttpCtx* ctx, goto l0_return; } - char content_length[24] = { 0 }; - snprintf(content_length, 24 - 1, "%ld", bytes_read); + char* buf = calloc(file_size + 1, sizeof(char)); + size_t bytes_read = fread(buf, sizeof(char), file_size, fp); + if (bytes_read != file_size) { + fprintf(stderr, "error: could not read file '%s'\n", filepath); + RESPOND_HTML_SERVER_ERROR(ctx); + res = -1; + goto l0_return; + } http_ctx_res_headers_set(ctx, "Content-Type", mime_type); - http_ctx_res_headers_set(ctx, "Content-Length", content_length); - http_ctx_respond(ctx, 200, buf); + http_ctx_respond_str(ctx, 200, buf); res = 0; l0_return: @@ -159,13 +278,11 @@ l0_return: void route_get_product_editor_html(HttpCtx* ctx) { read_and_send_file( - ctx, PUBLIC_DIR_PATH "/product_editor.html", 16384 - 1, "text/html"); + ctx, PUBLIC_DIR_PATH "/product_editor.html", "text/html"); } void route_get_product_editor_js(HttpCtx* ctx) { - read_and_send_file(ctx, - PUBLIC_DIR_PATH "/product_editor.js", - 16384 - 1, - "application/javascript"); + read_and_send_file( + ctx, PUBLIC_DIR_PATH "/product_editor.js", "application/javascript"); } diff --git a/backend/src/controllers/receipts.c b/backend/src/controllers/receipts.c index 7846436..25c9eb1 100644 --- a/backend/src/controllers/receipts.c +++ b/backend/src/controllers/receipts.c @@ -12,6 +12,10 @@ void route_get_receipts_one(HttpCtx* ctx) return; const char* query = http_ctx_req_query(ctx); + if (!query) { + RESPOND_BAD_REQUEST(ctx, "no receipt_id parameter"); + return; + } HttpQueryParams* params = http_parse_query_params(query); char* receipt_id_str = http_query_params_get(params, "receipt_id"); http_query_params_free(params); diff --git a/backend/src/controllers/sessions.c b/backend/src/controllers/sessions.c index 6edc744..ce29ea3 100644 --- a/backend/src/controllers/sessions.c +++ b/backend/src/controllers/sessions.c @@ -8,7 +8,7 @@ void route_post_sessions_login(HttpCtx* ctx) { Cx* cx = http_ctx_user_ctx(ctx); - const char* body_str = http_ctx_req_body(ctx); + const char* body_str = http_ctx_req_body_str(ctx); JsonValue* body_json = json_parse(body_str, strlen(body_str)); diff --git a/backend/src/controllers/users.c b/backend/src/controllers/users.c index e7b1b39..a475d43 100644 --- a/backend/src/controllers/users.c +++ b/backend/src/controllers/users.c @@ -8,7 +8,7 @@ void route_post_users_register(HttpCtx* ctx) { Cx* cx = http_ctx_user_ctx(ctx); - const char* body_str = http_ctx_req_body(ctx); + const char* body_str = http_ctx_req_body_str(ctx); JsonValue* body_json = json_parse(body_str, strlen(body_str)); diff --git a/backend/src/db/db.h b/backend/src/db/db.h index dbb25a3..2a4f9d4 100644 --- a/backend/src/db/db.h +++ b/backend/src/db/db.h @@ -67,3 +67,10 @@ DbRes db_receipt_prices( /// `products` field is an out parameter. /// Expects `products` to be constructed. DbRes db_receipt_products(Db* db, ProductVec* products, int64_t receipt_id); + +DbRes db_product_image_insert( + Db* db, int64_t product_id, const uint8_t* data, size_t data_size); +/// `data` and `data_size` are out parameters. +/// `*data` should be freed. +DbRes db_product_image_with_product_id( + Db* db, uint8_t** data, size_t* data_size, int64_t product_id); diff --git a/backend/src/db/db_sqlite.c b/backend/src/db/db_sqlite.c index 547fa97..2d0f747 100644 --- a/backend/src/db/db_sqlite.c +++ b/backend/src/db/db_sqlite.c @@ -6,6 +6,8 @@ #include #include #include +#include +#include #define REPORT_SQLITE3_ERROR() \ fprintf(stderr, \ @@ -925,3 +927,86 @@ l0_return: DISCONNECT; return res; } + +DbRes db_product_image_insert( + Db* db, int64_t product_id, const uint8_t* data, size_t data_size) +{ + sqlite3* connection; + CONNECT; + DbRes res; + + sqlite3_stmt* stmt; + int prepare_res = sqlite3_prepare_v2(connection, + "INSERT INTO product_images (product, data) " + "VALUES (?, ?)", + -1, + &stmt, + NULL); + if (prepare_res != SQLITE_OK) { + REPORT_SQLITE3_ERROR(); + res = DbRes_Error; + goto l0_return; + } + + sqlite3_bind_int64(stmt, 1, product_id); + sqlite3_bind_blob64(stmt, 2, data, data_size, NULL); + + int step_res = sqlite3_step(stmt); + if (step_res != SQLITE_DONE) { + REPORT_SQLITE3_ERROR(); + res = DbRes_Error; + goto l0_return; + } + + res = DbRes_Ok; +l0_return: + if (stmt) + sqlite3_finalize(stmt); + DISCONNECT; + return res; +} + +DbRes db_product_image_with_product_id( + Db* db, uint8_t** data, size_t* data_size, int64_t product_id) +{ + + sqlite3* connection; + CONNECT; + DbRes res; + + sqlite3_stmt* stmt; + int prepare_res = sqlite3_prepare_v2(connection, + "SELECT data" + " FROM product_images WHERE product = ?", + -1, + &stmt, + NULL); + if (prepare_res != SQLITE_OK) { + REPORT_SQLITE3_ERROR(); + res = DbRes_Error; + goto l0_return; + } + sqlite3_bind_int64(stmt, 1, product_id); + + int step_res = sqlite3_step(stmt); + if (step_res == SQLITE_DONE) { + res = DbRes_NotFound; + goto l0_return; + } else if (step_res != SQLITE_ROW) { + REPORT_SQLITE3_ERROR(); + res = DbRes_Error; + goto l0_return; + } + + *data_size = (size_t)sqlite3_column_bytes(stmt, 0); + *data = malloc(*data_size); + const void* db_data = sqlite3_column_blob(stmt, 0); + memcpy(*data, db_data, *data_size); + + res = DbRes_Ok; +l0_return: + if (stmt) + sqlite3_finalize(stmt); + DISCONNECT; + return res; +} diff --git a/backend/src/http/http.h b/backend/src/http/http.h index b7a42c5..3ca0613 100644 --- a/backend/src/http/http.h +++ b/backend/src/http/http.h @@ -34,9 +34,13 @@ const char* http_ctx_req_path(HttpCtx* ctx); bool http_ctx_req_headers_has(HttpCtx* ctx, const char* key); const char* http_ctx_req_headers_get(HttpCtx* ctx, const char* key); const char* http_ctx_req_query(HttpCtx* ctx); -const char* http_ctx_req_body(HttpCtx* ctx); +const char* http_ctx_req_body_str(HttpCtx* ctx); +const uint8_t* http_ctx_req_body(HttpCtx* ctx); +size_t http_ctx_req_body_size(HttpCtx* ctx); void http_ctx_res_headers_set(HttpCtx* ctx, const char* key, const char* value); -void http_ctx_respond(HttpCtx* ctx, int status, const char* body); +void http_ctx_respond_str(HttpCtx* ctx, int status, const char* body); +void http_ctx_respond( + HttpCtx* ctx, int status, const uint8_t* body, size_t body_size); typedef struct HttpQueryParams HttpQueryParams; diff --git a/backend/src/http/server.c b/backend/src/http/server.c index fd257d1..fc2583a 100644 --- a/backend/src/http/server.c +++ b/backend/src/http/server.c @@ -160,11 +160,21 @@ const char* http_ctx_req_query(HttpCtx* ctx) return ctx->req->query; } -const char* http_ctx_req_body(HttpCtx* ctx) +const char* http_ctx_req_body_str(HttpCtx* ctx) +{ + return (char*)ctx->req_body; +} + +const uint8_t* http_ctx_req_body(HttpCtx* ctx) { return ctx->req_body; } +size_t http_ctx_req_body_size(HttpCtx* ctx) +{ + return ctx->req_body_size; +} + void http_ctx_res_headers_set(HttpCtx* ctx, const char* key, const char* value) { char* key_copy = malloc(strlen(key) + 1); @@ -175,11 +185,21 @@ void http_ctx_res_headers_set(HttpCtx* ctx, const char* key, const char* value) header_vec_push(&ctx->res_headers, (Header) { key_copy, value_copy }); } -void http_ctx_respond(HttpCtx* ctx, int status, const char* body) +void http_ctx_respond_str(HttpCtx* ctx, int status, const char* body) +{ + http_ctx_respond(ctx, status, (const uint8_t*)body, strlen(body)); +} + +void http_ctx_respond( + HttpCtx* ctx, int status, const uint8_t* body, size_t body_size) { // https://httpwg.org/specs/rfc9112.html#persistent.tear-down http_ctx_res_headers_set(ctx, "Connection", "close"); + char content_length[24] = { 0 }; + snprintf(content_length, 24 - 1, "%ld", body_size); + http_ctx_res_headers_set(ctx, "Content-Length", content_length); + String res; string_construct(&res); @@ -199,12 +219,14 @@ void http_ctx_respond(HttpCtx* ctx, int status, const char* body) } string_push_str(&res, "\r\n"); - string_push_str(&res, body); - ssize_t bytes_written = write(ctx->client->file, res.data, res.size); if (bytes_written != (ssize_t)res.size) { - fprintf(stderr, "error: could not send response\n"); + fprintf(stderr, "error: could not send response header\n"); } - string_destroy(&res); + + bytes_written = write(ctx->client->file, body, body_size); + if (bytes_written != (ssize_t)body_size) { + fprintf(stderr, "error: could not send response body\n"); + } } diff --git a/backend/src/http/server.h b/backend/src/http/server.h index b4e0056..c41da00 100644 --- a/backend/src/http/server.h +++ b/backend/src/http/server.h @@ -37,7 +37,8 @@ struct HttpServer { struct HttpCtx { ClientConnection* client; const Request* req; - const char* req_body; + const uint8_t* req_body; + size_t req_body_size; HeaderVec res_headers; void* user_ctx; }; diff --git a/backend/src/http/worker.c b/backend/src/http/worker.c index 6fd4063..5ff5e3f 100644 --- a/backend/src/http/worker.c +++ b/backend/src/http/worker.c @@ -96,7 +96,8 @@ void http_worker_handle_connection(Worker* worker, ClientConnection connection) HttpCtx handler_ctx = { .client = &client->connection, .req = &request, - .req_body = (char*)request.body, + .req_body = request.body, + .req_body_size = request.body_size, .res_headers = { 0 }, .user_ctx = worker->ctx->server->user_ctx, }; diff --git a/backend/src/main.c b/backend/src/main.c index cc4a240..5d45ceb 100644 --- a/backend/src/main.c +++ b/backend/src/main.c @@ -42,6 +42,10 @@ int main(void) server, "/api/products/create", route_post_products_create); http_server_post( server, "/api/products/update", route_post_products_update); + http_server_post( + server, "/api/products/set-image", route_post_products_set_image); + http_server_get( + server, "/api/products/image.png", route_get_products_image_png); http_server_get( server, "/product_editor/index.html", route_get_product_editor_html);