diff options
| author | WriterV2 <primary-v2@protonmail.com> | 2024-10-28 19:28:41 +0100 |
|---|---|---|
| committer | WriterV2 <primary-v2@protonmail.com> | 2024-10-28 19:28:41 +0100 |
| commit | 977c93ef0568aa488c226915d14e6f164519fd98 (patch) | |
| tree | 276dd1237f51d1b7fab526b3f12a9b798d0f5209 | |
| parent | 04e19765aca471fd85199714699aaf827d04608d (diff) | |
Feat: Add admin API route to add story
| -rw-r--r-- | Cargo.lock | 104 | ||||
| -rw-r--r-- | Cargo.toml | 3 | ||||
| -rw-r--r-- | src/db/mod.rs | 16 | ||||
| -rw-r--r-- | src/handlers/admin.rs | 92 | ||||
| -rw-r--r-- | src/handlers/mod.rs | 16 | ||||
| -rw-r--r-- | src/main.rs | 2 |
6 files changed, 202 insertions, 31 deletions
@@ -100,9 +100,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axum" -version = "0.7.5" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae" dependencies = [ "async-trait", "axum-core", @@ -117,6 +117,7 @@ dependencies = [ "matchit", "memchr", "mime", + "multer", "percent-encoding", "pin-project-lite", "rustversion", @@ -126,7 +127,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper 1.0.1", "tokio", - "tower", + "tower 0.5.1", "tower-layer", "tower-service", "tracing", @@ -134,9 +135,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" dependencies = [ "async-trait", "bytes", @@ -147,7 +148,30 @@ dependencies = [ "mime", "pin-project-lite", "rustversion", - "sync_wrapper 0.1.2", + "sync_wrapper 1.0.1", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-extra" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73c3220b188aea709cf1b6c5f9b01c3bd936bb08bd2b5184a12b35ac8131b1f9" +dependencies = [ + "axum", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "serde", + "serde_html_form", + "tower 0.5.1", "tower-layer", "tower-service", "tracing", @@ -381,6 +405,15 @@ dependencies = [ ] [[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + +[[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -728,6 +761,8 @@ dependencies = [ "hyper", "pin-project-lite", "tokio", + "tower 0.4.13", + "tower-service", ] [[package]] @@ -919,6 +954,23 @@ dependencies = [ ] [[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] name = "native-tls" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1166,6 +1218,7 @@ version = "0.1.0" dependencies = [ "anyhow", "axum", + "axum-extra", "dotenvy", "maud", "serde", @@ -1374,6 +1427,19 @@ dependencies = [ ] [[package]] +name = "serde_html_form" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de514ef58196f1fc96dcaef80fe6170a1ce6215df9687a93fe8300e773fefc5" +dependencies = [ + "form_urlencoded", + "indexmap", + "itoa", + "ryu", + "serde", +] + +[[package]] name = "serde_json" version = "1.0.122" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1858,6 +1924,22 @@ dependencies = [ ] [[package]] +name = "tower" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 0.1.2", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] name = "tower-http" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1881,7 +1963,7 @@ dependencies = [ "pin-project-lite", "tokio", "tokio-util", - "tower", + "tower 0.4.13", "tower-layer", "tower-service", "tracing", @@ -1890,15 +1972,15 @@ dependencies = [ [[package]] name = "tower-layer" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" @@ -10,8 +10,9 @@ serde = {version = "1.0", features = ["derive"]} anyhow = "1.0" tokio = { version = "1.39.2", features = ["full"] } maud = { version = "0.26.0", features = ["axum"] } -axum = "0.7.5" +axum = { version = "0.7.5", features = ["multipart"] } tower-http = { version = "0.5.2", features = ["full", "fs"] } sqlx = { version = "0.8.2", features = ["runtime-tokio", "tls-native-tls", "sqlite", "macros"] } dotenvy = "0.15.7" subtle = "2.6.1" +axum-extra = { version = "0.9.4", features = ["query"] } diff --git a/src/db/mod.rs b/src/db/mod.rs index b5b1488..01111d8 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,8 +1,8 @@ -mod game; -mod gametag; -mod product; -mod story; -mod storytag; -mod tag; -mod tool; -mod tooltag; +pub mod game; +pub mod gametag; +pub mod product; +pub mod story; +pub mod storytag; +pub mod tag; +pub mod tool; +pub mod tooltag; 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<ApiContext>, + mut multipart: Multipart, +) -> Result<impl IntoResponse, AppError> { + let mut form: HashMap<String, Bytes> = 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 })) } diff --git a/src/main.rs b/src/main.rs index b4d7567..dfdf658 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ use std::env; use dotenvy::dotenv; use sqlx::sqlite::SqlitePoolOptions; -mod db; +pub mod db; mod error; mod handlers; |
