Fix S3 collision, alias clearing, upload limit, TOCTOU; add tests
Bug fixes:
- S3 key is now emoji/{uuid}.{ext} instead of emoji/{filename},
preventing silent overwrites when two emotes share a filename
- UpdateEmoteRequest.alias uses Option<Option<String>> with a custom
deserializer so a JSON null clears the alias rather than being ignored;
the manage UI now sends null when the alias field is emptied
- POST /emotes is limited to 8 MiB via DefaultBodyLimit
- update_emote replaced the fetch-then-update pair with a single
UPDATE … RETURNING using COALESCE/CASE WHEN, eliminating the TOCTOU
race between concurrent edits
Refactoring:
- Extracted src/lib.rs so domain logic is a library crate; src/main.rs
is now a thin startup entry point
- auth::check decoupled from AppState — takes Option<&AuthConfig> directly
- Removed unused config field from Database struct
Tests (40 total):
- auth: 10 unit tests covering all check() branches
- models: 6 unit tests for timestamp parsing and alias deserialization
- db: 9 unit tests against in-memory SQLite covering full CRUD
- routes: 15 integration tests in tests/routes.rs covering auth
middleware, input validation, and all mutating endpoints
This commit is contained in:
+3
-45
@@ -1,31 +1,9 @@
|
||||
mod auth;
|
||||
mod config;
|
||||
mod db;
|
||||
mod models;
|
||||
mod routes;
|
||||
mod storage;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{
|
||||
middleware,
|
||||
routing::{delete, get, post, put},
|
||||
Router,
|
||||
};
|
||||
use mikebase::{config::AppConfig, db::Database, storage::S3Storage, AppState, build_router};
|
||||
use sqlx::any::install_default_drivers;
|
||||
use tower_http::trace::TraceLayer;
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
use crate::{config::AppConfig, db::Database, storage::S3Storage};
|
||||
|
||||
/// Shared application state injected into every handler.
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub db: Database,
|
||||
pub storage: S3Storage,
|
||||
pub cfg: Arc<AppConfig>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Initialise structured logging.
|
||||
@@ -45,7 +23,7 @@ async fn main() {
|
||||
install_default_drivers();
|
||||
|
||||
// Connect to the database and run migrations.
|
||||
let db = Database::connect(cfg.clone())
|
||||
let db = Database::connect(&cfg.database.url)
|
||||
.await
|
||||
.expect("Failed to connect to database");
|
||||
db.migrate().await.expect("Failed to run migrations");
|
||||
@@ -55,32 +33,12 @@ async fn main() {
|
||||
|
||||
let state = AppState { db, storage, cfg: cfg.clone() };
|
||||
|
||||
let protected = Router::new()
|
||||
.route("/manage", get(routes::manage::manage_root))
|
||||
.route("/manage/emotes", get(routes::manage::list_admin_emotes))
|
||||
.route("/emotes", post(routes::emotes::create_emote))
|
||||
.route("/emotes/{uuid}", put(routes::emotes::update_emote))
|
||||
.route("/emotes/{uuid}", delete(routes::emotes::delete_emote))
|
||||
.layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
auth::require_basic_auth,
|
||||
));
|
||||
|
||||
let app = Router::new()
|
||||
.route("/", get(routes::emotes::root))
|
||||
.route("/health", get(routes::health::health))
|
||||
.route("/version", get(routes::version::version))
|
||||
.route("/json", get(routes::emotes::list_emotes))
|
||||
.merge(protected)
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.with_state(state);
|
||||
|
||||
let addr = format!("{}:{}", cfg.server.host, cfg.server.port);
|
||||
tracing::info!("Listening on {addr}");
|
||||
let listener = tokio::net::TcpListener::bind(&addr)
|
||||
.await
|
||||
.expect("Failed to bind address");
|
||||
axum::serve(listener, app)
|
||||
axum::serve(listener, build_router(state))
|
||||
.await
|
||||
.expect("Server error");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user