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
oabrivard 3 months ago
parent 467ad550a5
commit 10b8d950b9

@ -9,3 +9,4 @@ pub mod llm_logs;
pub mod settings;
pub mod sources;
pub mod syntheses;
pub mod themes;

@ -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)
}

@ -37,6 +37,11 @@ pub fn build_router(state: AppState, config: &AppConfig) -> Router {
// Settings routes (authenticated)
.route("/settings", get(handlers::settings::get_settings))
.route("/settings", put(handlers::settings::update_settings))
// Themes routes (authenticated)
.route("/themes", get(handlers::themes::list))
.route("/themes", post(handlers::themes::create))
.route("/themes/{id}", put(handlers::themes::update))
.route("/themes/{id}", delete(handlers::themes::delete))
// Sources routes (authenticated)
.route("/sources", get(handlers::sources::list))
.route("/sources", post(handlers::sources::create))

Loading…
Cancel
Save