summaryrefslogblamecommitdiff
path: root/makima/src/server/handlers/settings.rs
blob: ae52d5a45b97dc64db9daecadacf2bb5c2409689 (plain) (tree)



































































































































































































                                                                                                
//! HTTP handlers for user settings (feature flags / preferences).

use axum::{
    extract::{Path, State},
    http::StatusCode,
    response::IntoResponse,
    Json,
};

use crate::db::models::{UpsertUserSettingRequest, UserSettingsResponse};
use crate::db::repository;
use crate::server::auth::Authenticated;
use crate::server::messages::ApiError;
use crate::server::state::SharedState;

/// List all settings for the authenticated user.
#[utoipa::path(
    get,
    path = "/api/v1/settings",
    responses(
        (status = 200, description = "User settings", body = UserSettingsResponse),
        (status = 401, description = "Not authenticated", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
        (status = 500, description = "Internal server error", body = ApiError),
    ),
    security(
        ("bearer_auth" = [])
    ),
    tag = "Settings"
)]
pub async fn list_settings(
    State(state): State<SharedState>,
    Authenticated(user): Authenticated,
) -> impl IntoResponse {
    let Some(ref pool) = state.db_pool else {
        return (
            StatusCode::SERVICE_UNAVAILABLE,
            Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
        )
            .into_response();
    };

    match repository::get_user_settings(pool, user.owner_id).await {
        Ok(settings) => Json(UserSettingsResponse { settings }).into_response(),
        Err(e) => {
            tracing::error!("Failed to list user settings: {}", e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response()
        }
    }
}

/// Get a specific setting by key.
#[utoipa::path(
    get,
    path = "/api/v1/settings/{key}",
    params(
        ("key" = String, Path, description = "Setting key")
    ),
    responses(
        (status = 200, description = "User setting", body = crate::db::models::UserSetting),
        (status = 401, description = "Not authenticated", body = ApiError),
        (status = 404, description = "Setting not found", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
        (status = 500, description = "Internal server error", body = ApiError),
    ),
    security(
        ("bearer_auth" = [])
    ),
    tag = "Settings"
)]
pub async fn get_setting(
    State(state): State<SharedState>,
    Authenticated(user): Authenticated,
    Path(key): Path<String>,
) -> impl IntoResponse {
    let Some(ref pool) = state.db_pool else {
        return (
            StatusCode::SERVICE_UNAVAILABLE,
            Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
        )
            .into_response();
    };

    match repository::get_user_setting(pool, user.owner_id, &key).await {
        Ok(Some(setting)) => Json(setting).into_response(),
        Ok(None) => (
            StatusCode::NOT_FOUND,
            Json(ApiError::new("NOT_FOUND", format!("Setting '{}' not found", key))),
        )
            .into_response(),
        Err(e) => {
            tracing::error!("Failed to get user setting: {}", e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response()
        }
    }
}

/// Upsert a user setting (create or update).
#[utoipa::path(
    put,
    path = "/api/v1/settings",
    request_body = UpsertUserSettingRequest,
    responses(
        (status = 200, description = "Setting upserted", body = crate::db::models::UserSetting),
        (status = 401, description = "Not authenticated", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
        (status = 500, description = "Internal server error", body = ApiError),
    ),
    security(
        ("bearer_auth" = [])
    ),
    tag = "Settings"
)]
pub async fn upsert_setting(
    State(state): State<SharedState>,
    Authenticated(user): Authenticated,
    Json(req): Json<UpsertUserSettingRequest>,
) -> impl IntoResponse {
    let Some(ref pool) = state.db_pool else {
        return (
            StatusCode::SERVICE_UNAVAILABLE,
            Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
        )
            .into_response();
    };

    match repository::upsert_user_setting(pool, user.owner_id, &req.key, &req.value).await {
        Ok(setting) => Json(setting).into_response(),
        Err(e) => {
            tracing::error!("Failed to upsert user setting: {}", e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response()
        }
    }
}

/// Delete a user setting by key.
#[utoipa::path(
    delete,
    path = "/api/v1/settings/{key}",
    params(
        ("key" = String, Path, description = "Setting key")
    ),
    responses(
        (status = 200, description = "Setting deleted"),
        (status = 401, description = "Not authenticated", body = ApiError),
        (status = 404, description = "Setting not found", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
        (status = 500, description = "Internal server error", body = ApiError),
    ),
    security(
        ("bearer_auth" = [])
    ),
    tag = "Settings"
)]
pub async fn delete_setting(
    State(state): State<SharedState>,
    Authenticated(user): Authenticated,
    Path(key): Path<String>,
) -> impl IntoResponse {
    let Some(ref pool) = state.db_pool else {
        return (
            StatusCode::SERVICE_UNAVAILABLE,
            Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
        )
            .into_response();
    };

    match repository::delete_user_setting(pool, user.owner_id, &key).await {
        Ok(true) => StatusCode::OK.into_response(),
        Ok(false) => (
            StatusCode::NOT_FOUND,
            Json(ApiError::new("NOT_FOUND", format!("Setting '{}' not found", key))),
        )
            .into_response(),
        Err(e) => {
            tracing::error!("Failed to delete user setting: {}", e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response()
        }
    }
}