raisfast 0.2.23

The last backend you'll ever need. Rust-powered headless CMS with built-in blog, ecommerce, wallet, payment and 4 plugin engines.
//! Site options API handler

use std::collections::HashMap;

use axum::Json;
use axum::extract::{Path, State};
use serde_json::Value;

use crate::AppState;
use crate::dto::{UpdateOptionRequest, UpdateOptionsRequest};
use crate::errors::app_error::{AppError, AppResult};
use crate::errors::response::ApiResponse;

pub fn routes(
    registry: &mut crate::server::RouteRegistry,
    config: &crate::config::app::AppConfig,
) -> axum::Router<crate::AppState> {
    let restful = config.api_restful;
    let r = axum::Router::new();
    let r = reg_route!(
        r,
        registry,
        restful,
        "/options/public",
        get,
        get_public_options,
        "system public",
        "options"
    );
    let r = reg_route!(
        r,
        registry,
        restful,
        "/admin/options",
        get,
        list_options,
        "system admin",
        "admin/options"
    );
    let r = reg_route!(
        r,
        registry,
        restful,
        "/admin/options",
        put,
        update_options,
        "system admin",
        "admin/options"
    );
    let r = reg_route!(
        r,
        registry,
        restful,
        "/admin/options/{key}",
        get,
        get_option,
        "system admin",
        "admin/options"
    );
    let r = reg_route!(
        r,
        registry,
        restful,
        "/admin/options/{key}",
        put,
        set_option,
        "system admin",
        "admin/options"
    );
    reg_route!(
        r,
        registry,
        restful,
        "/admin/options/{key}",
        delete,
        delete_option,
        "system admin",
        "admin/options"
    )
}

/// GET /options/public — Public options (values only) + system feature flags
#[utoipa::path(get, path = "/options/public", tag = "options",
    responses((status = 200, description = "Public options"))
)]
pub async fn get_public_options(
    State(state): State<AppState>,
) -> AppResult<ApiResponse<HashMap<String, Value>>> {
    let mut options = state.options.get_public().await;
    options.insert(
        "builtin_tenantable".into(),
        Value::Bool(state.config.builtin_tenantable),
    );
    Ok(ApiResponse::success(options))
}

/// GET /admin/options — All options (grouped, with metadata)
#[utoipa::path(get, path = "/admin/options", tag = "options",
    security(("bearer_auth" = [])),
    responses((status = 200, description = "All options grouped"))
)]
pub async fn list_options(
    State(state): State<AppState>,
) -> AppResult<ApiResponse<Vec<crate::services::options::OptionGroup>>> {
    let groups = state.options.get_grouped().await?;
    Ok(ApiResponse::success(groups))
}

/// GET /admin/options/:key — Get a single option
#[utoipa::path(get, path = "/admin/options/{key}", tag = "options",
    security(("bearer_auth" = [])),
    params(("key" = String, Path, description = "Option key")),
    responses((status = 200, description = "Option value"))
)]
pub async fn get_option(
    State(state): State<AppState>,
    Path(key): Path<String>,
) -> AppResult<ApiResponse<serde_json::Value>> {
    let entry = state
        .options
        .get_entry(&key)
        .await
        .ok_or_else(|| AppError::not_found(&format!("option/{key}")))?;
    Ok(ApiResponse::success(
        serde_json::to_value(entry).map_err(|e| AppError::Internal(anyhow::anyhow!("{e}")))?,
    ))
}

/// PUT /admin/options — Batch update options
#[utoipa::path(put, path = "/admin/options", tag = "options",
    security(("bearer_auth" = [])),
    responses((status = 200, description = "Options updated"))
)]
pub async fn update_options(
    State(state): State<AppState>,
    Json(body): Json<UpdateOptionsRequest>,
) -> AppResult<ApiResponse<Vec<crate::services::options::OptionGroup>>> {
    state.options.set_batch(body.options).await?;
    let groups = state.options.get_grouped().await?;
    Ok(ApiResponse::success(groups))
}

/// PUT /admin/options/:key — Set a single option
#[utoipa::path(put, path = "/admin/options/{key}", tag = "options",
    security(("bearer_auth" = [])),
    params(("key" = String, Path, description = "Option key")),
    responses((status = 200, description = "Option set"))
)]
pub async fn set_option(
    State(state): State<AppState>,
    Path(key): Path<String>,
    Json(body): Json<UpdateOptionRequest>,
) -> AppResult<ApiResponse<serde_json::Value>> {
    state.options.set(&key, body.value).await?;
    Ok(ApiResponse::success(serde_json::json!({
        "option_key": key,
        "updated": true,
    })))
}

/// DELETE /admin/options/:key — Delete an option
#[utoipa::path(delete, path = "/admin/options/{key}", tag = "options",
    security(("bearer_auth" = [])),
    params(("key" = String, Path, description = "Option key")),
    responses((status = 200, description = "Option deleted"))
)]
pub async fn delete_option(
    State(state): State<AppState>,
    Path(key): Path<String>,
) -> AppResult<ApiResponse<serde_json::Value>> {
    state.options.delete(&key).await?;
    Ok(ApiResponse::success(serde_json::json!({
        "option_key": key,
        "deleted": true,
    })))
}