Skip to main content

tuitbot_server/routes/settings/
mod.rs

1//! Settings endpoints for reading and updating the configuration.
2//!
3//! ## Module layout
4//! - `handlers`   — all route handlers (onboarding, config, factory reset)
5//! - `validation` — shared helpers, TOML merge/convert utilities
6
7pub mod handlers;
8pub mod validation;
9
10#[cfg(test)]
11mod tests;
12// tests/ is a submodule directory: tests/mod.rs → toml, helpers
13
14// Re-export public API so the router can reference `settings::*` unchanged.
15pub use handlers::{config_status, get_settings, init_settings, patch_settings, validate_settings};
16pub use validation::{factory_reset, get_defaults, merge_patch_and_parse, test_llm};
17
18use serde::{Deserialize, Serialize};
19use tuitbot_core::error::ConfigError;
20
21// ---------------------------------------------------------------------------
22// LLM test request/response types
23// ---------------------------------------------------------------------------
24
25#[derive(Deserialize)]
26pub struct TestLlmRequest {
27    pub provider: String,
28    #[serde(default)]
29    pub api_key: Option<String>,
30    #[serde(default)]
31    pub model: String,
32    #[serde(default)]
33    pub base_url: Option<String>,
34}
35
36// ---------------------------------------------------------------------------
37// Factory reset types + constant (used by handlers + tests)
38// ---------------------------------------------------------------------------
39
40/// Confirmation phrase required for factory reset (case-sensitive, exact match).
41pub(super) const FACTORY_RESET_PHRASE: &str = "RESET TUITBOT";
42
43#[derive(Deserialize)]
44pub struct FactoryResetRequest {
45    pub confirmation: String,
46}
47
48#[derive(Serialize)]
49pub(super) struct FactoryResetResponse {
50    pub status: String,
51    pub cleared: FactoryResetCleared,
52}
53
54#[derive(Serialize)]
55pub(super) struct FactoryResetCleared {
56    pub tables_cleared: u32,
57    pub rows_deleted: u64,
58    pub config_deleted: bool,
59    pub passphrase_deleted: bool,
60    pub media_deleted: bool,
61    pub credentials_deleted: bool,
62    pub runtimes_stopped: u32,
63}
64
65// ---------------------------------------------------------------------------
66// Request / response types (shared across handlers + validation)
67// ---------------------------------------------------------------------------
68
69/// Request body for the optional claim object within `POST /api/settings/init`.
70#[derive(Deserialize)]
71pub(super) struct ClaimRequest {
72    pub passphrase: String,
73}
74
75/// X profile data passed from the frontend during onboarding init.
76///
77/// Populated from the OAuth callback response so we can write the user's
78/// X identity to the default account row atomically with config creation.
79#[derive(Deserialize)]
80pub(super) struct XProfileData {
81    pub x_user_id: String,
82    pub x_username: String,
83    pub x_display_name: String,
84    #[serde(default)]
85    pub x_avatar_url: Option<String>,
86}
87
88#[derive(Serialize)]
89pub(super) struct ValidationResponse {
90    pub valid: bool,
91    #[serde(skip_serializing_if = "Vec::is_empty")]
92    pub errors: Vec<ValidationErrorItem>,
93}
94
95#[derive(Serialize)]
96pub(super) struct ValidationErrorItem {
97    pub field: String,
98    pub message: String,
99}
100
101#[derive(Serialize)]
102pub(super) struct TestResult {
103    pub success: bool,
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub error: Option<String>,
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub latency_ms: Option<u64>,
108}
109
110// ---------------------------------------------------------------------------
111// Shared helper (used by handlers + tests)
112// ---------------------------------------------------------------------------
113
114pub(super) fn config_errors_to_response(errors: Vec<ConfigError>) -> Vec<ValidationErrorItem> {
115    errors
116        .into_iter()
117        .map(|e| match e {
118            ConfigError::MissingField { field } => ValidationErrorItem {
119                field,
120                message: "this field is required".to_string(),
121            },
122            ConfigError::InvalidValue { field, message } => ValidationErrorItem { field, message },
123            other => ValidationErrorItem {
124                field: String::new(),
125                message: other.to_string(),
126            },
127        })
128        .collect()
129}