feat: add themes CRUD endpoints
Implements GET/POST/PUT/DELETE /api/v1/themes handlers following the same patterns as sources.rs, registers the module, and wires up routes in the router. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>master
parent
467ad550a5
commit
10b8d950b9
@ -0,0 +1,127 @@
|
||||
//! Themes handlers.
|
||||
//!
|
||||
//! - `GET /api/v1/themes` — list user's themes
|
||||
//! - `POST /api/v1/themes` — create a theme
|
||||
//! - `PUT /api/v1/themes/:id` — update a theme (partial)
|
||||
//! - `DELETE /api/v1/themes/:id` — delete a theme (ownership check)
|
||||
|
||||
use axum::extract::{Path, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::IntoResponse;
|
||||
use axum::Json;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::app_state::AppState;
|
||||
use crate::db;
|
||||
use crate::errors::AppError;
|
||||
use crate::middleware::auth::AuthUser;
|
||||
use crate::models::theme::{CreateThemeRequest, ThemeResponse, UpdateThemeRequest};
|
||||
|
||||
/// `GET /api/v1/themes`
|
||||
///
|
||||
/// Returns all themes belonging to the authenticated user,
|
||||
/// ordered by creation date (oldest first).
|
||||
pub async fn list(
|
||||
auth_user: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let themes = db::themes::list_for_user(&state.pool, auth_user.id).await?;
|
||||
let response: Vec<ThemeResponse> = themes
|
||||
.into_iter()
|
||||
.map(ThemeResponse::try_from)
|
||||
.collect::<Result<_, _>>()?;
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
/// `POST /api/v1/themes`
|
||||
///
|
||||
/// Creates a theme for the authenticated user.
|
||||
/// Validates the request fields before inserting.
|
||||
pub async fn create(
|
||||
auth_user: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Json(body): Json<CreateThemeRequest>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
body.validate().map_err(AppError::Validation)?;
|
||||
|
||||
let categories = serde_json::to_value(&body.categories)
|
||||
.map_err(|e| AppError::Internal(anyhow::anyhow!("Failed to serialize categories: {}", e)))?;
|
||||
|
||||
let theme = db::themes::create(
|
||||
&state.pool,
|
||||
auth_user.id,
|
||||
&body.name,
|
||||
&body.theme,
|
||||
&categories,
|
||||
body.max_items_per_category.unwrap_or(5),
|
||||
body.max_age_days.unwrap_or(7),
|
||||
body.summary_length.unwrap_or(2),
|
||||
)
|
||||
.await?;
|
||||
|
||||
tracing::info!(user_id = %auth_user.id, theme_id = %theme.id, "Theme created");
|
||||
|
||||
let response = ThemeResponse::try_from(theme)?;
|
||||
Ok((StatusCode::CREATED, Json(response)))
|
||||
}
|
||||
|
||||
/// `PUT /api/v1/themes/:id`
|
||||
///
|
||||
/// Partially updates a theme by ID. Returns 404 if the theme doesn't exist
|
||||
/// or doesn't belong to the current user.
|
||||
pub async fn update(
|
||||
auth_user: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(body): Json<UpdateThemeRequest>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let categories = body
|
||||
.categories
|
||||
.as_ref()
|
||||
.map(|cats| {
|
||||
serde_json::to_value(cats)
|
||||
.map_err(|e| AppError::Internal(anyhow::anyhow!("Failed to serialize categories: {}", e)))
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
let theme = db::themes::update(
|
||||
&state.pool,
|
||||
auth_user.id,
|
||||
id,
|
||||
body.name.as_deref(),
|
||||
body.theme.as_deref(),
|
||||
categories.as_ref(),
|
||||
body.max_items_per_category,
|
||||
body.max_age_days,
|
||||
body.summary_length,
|
||||
)
|
||||
.await?;
|
||||
|
||||
match theme {
|
||||
Some(t) => {
|
||||
tracing::info!(user_id = %auth_user.id, theme_id = %id, "Theme updated");
|
||||
Ok(Json(ThemeResponse::try_from(t)?))
|
||||
}
|
||||
None => Err(AppError::NotFound("Theme not found".into())),
|
||||
}
|
||||
}
|
||||
|
||||
/// `DELETE /api/v1/themes/:id`
|
||||
///
|
||||
/// Deletes a theme by ID. Returns 404 (not 403) if the theme doesn't exist
|
||||
/// or doesn't belong to the current user, to avoid leaking information about
|
||||
/// other users' themes.
|
||||
pub async fn delete(
|
||||
auth_user: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let deleted = db::themes::delete(&state.pool, auth_user.id, id).await?;
|
||||
|
||||
if !deleted {
|
||||
return Err(AppError::NotFound("Theme not found".into()));
|
||||
}
|
||||
|
||||
tracing::info!(user_id = %auth_user.id, theme_id = %id, "Theme deleted");
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
Loading…
Reference in New Issue