From 977c93ef0568aa488c226915d14e6f164519fd98 Mon Sep 17 00:00:00 2001 From: WriterV2 Date: Mon, 28 Oct 2024 19:28:41 +0100 Subject: Feat: Add admin API route to add story --- src/handlers/admin.rs | 92 +++++++++++++++++++++++++++++++++++++++++++++++---- src/handlers/mod.rs | 16 ++++++--- 2 files changed, 98 insertions(+), 10 deletions(-) (limited to 'src/handlers') diff --git a/src/handlers/admin.rs b/src/handlers/admin.rs index 349275b..bdb2b40 100644 --- a/src/handlers/admin.rs +++ b/src/handlers/admin.rs @@ -1,11 +1,21 @@ +use std::collections::HashMap; use std::env; -use axum::extract::Request; +use anyhow::Context; +use axum::body::Bytes; +use axum::extract::{Multipart, Request}; use axum::http::{HeaderMap, StatusCode}; use axum::middleware::Next; -use axum::response::Response; +use axum::response::{IntoResponse, Response}; +use axum::{Extension, Json}; use subtle::ConstantTimeEq; +use crate::db::product::Product; +use crate::db::story::Story; +use crate::error::AppError; + +use super::ApiContext; + pub async fn admin_middleware( headers: HeaderMap, request: Request, @@ -21,9 +31,9 @@ pub async fn admin_middleware( .and_then(|header| header.to_str().ok()) .ok_or(StatusCode::UNAUTHORIZED)?; - if !expected_token + if expected_token .as_bytes() - .ct_eq(token.as_bytes()) + .ct_ne(token.as_bytes()) .unwrap_u8() == 1 { @@ -34,6 +44,76 @@ pub async fn admin_middleware( Ok(next.run(request).await) } -pub async fn admin_handler() -> &'static str { - "Hello World" +pub async fn admin_healthcheck() -> impl IntoResponse { + StatusCode::OK +} + +pub async fn upload_story( + ctx: Extension, + mut multipart: Multipart, +) -> Result { + let mut form: HashMap = HashMap::new(); + while let Some(field) = multipart.next_field().await? { + if let Some(label) = field.name() { + form.insert(label.to_string(), field.bytes().await?); + } + } + + let name = if let Some(name_value) = form.get("name") { + String::from_utf8(name_value.to_vec()).with_context(|| "Failed to parse name")? + } else { + return Ok(StatusCode::BAD_REQUEST.into_response()); + }; + + let description = if let Some(description_value) = form.get("description") { + String::from_utf8(description_value.to_vec()) + .with_context(|| "Failed to parse description")? + } else { + return Ok(StatusCode::BAD_REQUEST.into_response()); + }; + + let language = if let Some(language_value) = form.get("language") { + String::from_utf8(language_value.to_vec()).with_context(|| "Failed to parse language")? + } else { + return Ok(StatusCode::BAD_REQUEST.into_response()); + }; + + let pdf = if let Some(pdf_value) = form.get("pdf") { + pdf_value.to_vec() + } else { + return Ok(StatusCode::BAD_REQUEST.into_response()); + }; + + let epub = if let Some(epub_value) = form.get("epub") { + epub_value.to_vec() + } else { + return Ok(StatusCode::BAD_REQUEST.into_response()); + }; + + let time = std::time::SystemTime::now(); + let uploaddate = time.duration_since(std::time::UNIX_EPOCH)?.as_millis() as i64; + + let mut tx = ctx.pool.begin().await?; + let product = sqlx::query_as!( + Product, + "INSERT INTO product (name, description, uploaddate, updatedate) VALUES ($1, $2, $3, $4) RETURNING id, name, description, uploaddate, updatedate", + name, + description, + uploaddate, + uploaddate) + .fetch_one(&mut *tx) + .await?; + + let story = sqlx::query_as!( + Story, + "INSERT INTO story (language, pdf, epub, pid) VALUES ($1, $2, $3, $4) RETURNING id, language, pdf, epub, pid", language, + pdf, + epub, + product.id) + .fetch_one(&mut *tx) + .await?; + + tx.commit().await?; + + Ok((StatusCode::CREATED, Json(story)).into_response()) } diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index d69f590..d346512 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -1,21 +1,29 @@ use axum::extract::DefaultBodyLimit; use axum::middleware::{self}; -use axum::routing::get; +use axum::routing::{get, post}; use axum::Router; use sqlx::SqlitePool; +use tower_http::add_extension::AddExtensionLayer; use tower_http::services::ServeDir; -use admin::{admin_handler, admin_middleware}; +use admin::{admin_healthcheck, admin_middleware, upload_story}; use frontend::home; mod admin; mod frontend; +#[derive(Debug, Clone)] +struct ApiContext { + pool: SqlitePool, +} + pub async fn router(pool: SqlitePool) -> Router { Router::new() - .route("/admin", get(admin_handler)) + .route("/admin/story", post(upload_story)) + .route("/admin", get(admin_healthcheck)) .layer(middleware::from_fn(admin_middleware)) .route("/", get(home)) .nest_service("/static", ServeDir::new("src/static")) - .layer(DefaultBodyLimit::max(10000)) + .layer(DefaultBodyLimit::max(1074000000)) + .layer(AddExtensionLayer::new(ApiContext { pool })) } -- cgit v1.2.3