//! Manga reactions (like/dislike) — a private, per-user taste signal. //! Writes require auth; the read is scoped under `/me/` so the URL can't be //! used to peek at another user's reactions. use axum::extract::{Path, State}; use axum::http::StatusCode; use axum::routing::{get, put}; use axum::{Json, Router}; use serde::Deserialize; use serde_json::json; use uuid::Uuid; use crate::app::AppState; use crate::auth::extractor::CurrentUser; use crate::domain::reaction::{MangaReaction, Reaction}; use crate::error::{AppError, AppResult}; use crate::repo; pub fn routes() -> Router { Router::new() .route( "/mangas/:id/reaction", put(set_reaction).delete(clear_reaction), ) .route("/me/reactions/:manga_id", get(get_reaction)) } #[derive(Debug, Deserialize)] pub struct SetReactionBody { pub reaction: String, } async fn set_reaction( State(state): State, CurrentUser(user): CurrentUser, Path(manga_id): Path, Json(body): Json, ) -> AppResult> { // Validate against the closed vocabulary here so a bad value is a clean // 422 rather than relying on the DB CHECK to surface as a 500. let reaction = Reaction::parse(&body.reaction).ok_or_else(|| AppError::ValidationFailed { message: "reaction must be 'like' or 'dislike'".into(), details: json!({ "reaction": "must be 'like' or 'dislike'" }), })?; // Unknown manga → 404 via the FK-violation mapping in repo::reaction. repo::reaction::upsert(&state.db, user.id, manga_id, reaction).await?; Ok(Json(MangaReaction { manga_id, reaction: Some(reaction), })) } async fn clear_reaction( State(state): State, CurrentUser(user): CurrentUser, Path(manga_id): Path, ) -> AppResult { repo::reaction::clear(&state.db, user.id, manga_id).await?; Ok(StatusCode::NO_CONTENT) } async fn get_reaction( State(state): State, CurrentUser(user): CurrentUser, Path(manga_id): Path, ) -> AppResult> { let reaction = repo::reaction::get(&state.db, user.id, manga_id).await?; Ok(Json(MangaReaction { manga_id, reaction })) }