Skip to main content

tuitbot_server/
lib.rs

1//! Tuitbot HTTP API server.
2//!
3//! Exposes `tuitbot-core`'s storage layer as a REST API with read + write
4//! endpoints, multi-strategy auth (bearer token + session cookie), and a
5//! WebSocket for real-time events.
6
7pub mod account;
8pub mod auth;
9pub mod dashboard;
10pub mod error;
11pub mod routes;
12pub mod state;
13pub mod ws;
14
15use std::sync::Arc;
16
17use axum::extract::DefaultBodyLimit;
18use axum::middleware;
19use axum::routing::{delete, get, patch, post};
20use axum::Router;
21use tower_http::cors::CorsLayer;
22use tower_http::trace::TraceLayer;
23
24use crate::state::AppState;
25
26/// Build the complete axum router with all API routes and middleware.
27pub fn build_router(state: Arc<AppState>) -> Router {
28    let api = Router::new()
29        .route("/health", get(routes::health::health))
30        .route("/health/detailed", get(routes::health::health_detailed))
31        // Auth
32        .route("/auth/login", post(auth::routes::login))
33        .route("/auth/logout", post(auth::routes::logout))
34        .route("/auth/status", get(auth::routes::status))
35        // Analytics
36        .route("/analytics/summary", get(routes::analytics::summary))
37        .route("/analytics/followers", get(routes::analytics::followers))
38        .route(
39            "/analytics/performance",
40            get(routes::analytics::performance),
41        )
42        .route("/analytics/topics", get(routes::analytics::topics))
43        .route(
44            "/analytics/recent-performance",
45            get(routes::analytics::recent_performance),
46        )
47        // Approval
48        .route("/approval/export", get(routes::approval::export_items))
49        .route("/approval", get(routes::approval::list_items))
50        .route("/approval/stats", get(routes::approval::stats))
51        .route("/approval/approve-all", post(routes::approval::approve_all))
52        .route(
53            "/approval/{id}/history",
54            get(routes::approval::get_edit_history),
55        )
56        .route("/approval/{id}", patch(routes::approval::edit_item))
57        .route(
58            "/approval/{id}/approve",
59            post(routes::approval::approve_item),
60        )
61        .route("/approval/{id}/reject", post(routes::approval::reject_item))
62        // Activity
63        .route("/activity/export", get(routes::activity::export_activity))
64        .route("/activity", get(routes::activity::list_activity))
65        .route(
66            "/activity/rate-limits",
67            get(routes::activity::rate_limit_usage),
68        )
69        // Replies
70        .route("/replies", get(routes::replies::list_replies))
71        // Content
72        .route(
73            "/content/tweets",
74            get(routes::content::list_tweets).post(routes::content::compose_tweet),
75        )
76        .route(
77            "/content/threads",
78            get(routes::content::list_threads).post(routes::content::compose_thread),
79        )
80        .route("/content/calendar", get(routes::content::calendar))
81        .route("/content/schedule", get(routes::content::schedule))
82        .route("/content/compose", post(routes::content::compose))
83        .route(
84            "/content/scheduled/{id}",
85            patch(routes::content::edit_scheduled).delete(routes::content::cancel_scheduled),
86        )
87        // Draft Studio — Tags (literal paths before parameterized)
88        .route(
89            "/tags",
90            get(routes::content::list_account_tags).post(routes::content::create_account_tag),
91        )
92        // Draft Studio (new canonical paths)
93        .route(
94            "/drafts",
95            get(routes::content::list_studio_drafts).post(routes::content::create_studio_draft),
96        )
97        .route(
98            "/drafts/{id}",
99            get(routes::content::get_studio_draft)
100                .patch(routes::content::autosave_draft)
101                .delete(routes::content::delete_draft),
102        )
103        .route(
104            "/drafts/{id}/meta",
105            patch(routes::content::patch_draft_meta),
106        )
107        .route(
108            "/drafts/{id}/schedule",
109            post(routes::content::schedule_studio_draft),
110        )
111        .route(
112            "/drafts/{id}/reschedule",
113            patch(routes::content::reschedule_studio_draft),
114        )
115        .route(
116            "/drafts/{id}/unschedule",
117            post(routes::content::unschedule_studio_draft),
118        )
119        .route(
120            "/drafts/{id}/archive",
121            post(routes::content::archive_studio_draft),
122        )
123        .route(
124            "/drafts/{id}/restore",
125            post(routes::content::restore_studio_draft),
126        )
127        .route(
128            "/drafts/{id}/duplicate",
129            post(routes::content::duplicate_studio_draft),
130        )
131        .route(
132            "/drafts/{id}/revisions",
133            get(routes::content::list_draft_revisions).post(routes::content::create_draft_revision),
134        )
135        .route(
136            "/drafts/{id}/revisions/{rev_id}/restore",
137            post(routes::content::restore_from_revision),
138        )
139        .route(
140            "/drafts/{id}/activity",
141            get(routes::content::list_draft_activity),
142        )
143        .route("/drafts/{id}/tags", get(routes::content::list_draft_tags))
144        .route(
145            "/drafts/{id}/tags/{tag_id}",
146            post(routes::content::assign_draft_tag).delete(routes::content::unassign_draft_tag),
147        )
148        // Legacy drafts (backward compat)
149        .route(
150            "/content/drafts",
151            get(routes::content::list_drafts).post(routes::content::create_draft),
152        )
153        .route(
154            "/content/drafts/{id}",
155            patch(routes::content::edit_draft).delete(routes::content::delete_draft),
156        )
157        .route(
158            "/content/drafts/{id}/schedule",
159            post(routes::content::schedule_draft),
160        )
161        .route(
162            "/content/drafts/{id}/publish",
163            post(routes::content::publish_draft),
164        )
165        // Ingest
166        .route("/ingest", post(routes::ingest::ingest))
167        // Sources
168        .route("/sources/status", get(routes::sources::source_status))
169        .route(
170            "/sources/{id}/reindex",
171            post(routes::sources::reindex_source),
172        )
173        // Targets
174        .route(
175            "/targets",
176            get(routes::targets::list_targets).post(routes::targets::add_target),
177        )
178        .route(
179            "/targets/{username}/timeline",
180            get(routes::targets::target_timeline),
181        )
182        .route(
183            "/targets/{username}/stats",
184            get(routes::targets::target_stats),
185        )
186        .route(
187            "/targets/{username}",
188            delete(routes::targets::remove_target),
189        )
190        // Strategy
191        .route("/strategy/current", get(routes::strategy::current))
192        .route("/strategy/history", get(routes::strategy::history))
193        .route("/strategy/refresh", post(routes::strategy::refresh))
194        .route("/strategy/inputs", get(routes::strategy::inputs))
195        // Costs — LLM
196        .route("/costs/summary", get(routes::costs::summary))
197        .route("/costs/daily", get(routes::costs::daily))
198        .route("/costs/by-model", get(routes::costs::by_model))
199        .route("/costs/by-type", get(routes::costs::by_type))
200        // Costs — X API
201        .route("/costs/x-api/summary", get(routes::costs::x_api_summary))
202        .route("/costs/x-api/daily", get(routes::costs::x_api_daily))
203        .route(
204            "/costs/x-api/by-endpoint",
205            get(routes::costs::x_api_by_endpoint),
206        )
207        // AI Assist
208        .route("/assist/tweet", post(routes::assist::assist_tweet))
209        .route("/assist/reply", post(routes::assist::assist_reply))
210        .route("/assist/thread", post(routes::assist::assist_thread))
211        .route("/assist/improve", post(routes::assist::assist_improve))
212        .route("/assist/topics", get(routes::assist::assist_topics))
213        .route(
214            "/assist/optimal-times",
215            get(routes::assist::assist_optimal_times),
216        )
217        .route("/assist/mode", get(routes::assist::get_mode))
218        // Vault
219        .route("/vault/sources", get(routes::vault::vault_sources))
220        .route("/vault/notes", get(routes::vault::search_notes))
221        .route("/vault/notes/{id}", get(routes::vault::note_detail))
222        .route("/vault/search", get(routes::vault::search_fragments))
223        .route("/vault/resolve-refs", post(routes::vault::resolve_refs))
224        // Discovery feed
225        .route("/discovery/feed", get(routes::discovery::feed))
226        .route("/discovery/keywords", get(routes::discovery::keywords))
227        .route(
228            "/discovery/{tweet_id}/compose-reply",
229            post(routes::discovery::compose_reply),
230        )
231        .route(
232            "/discovery/{tweet_id}/queue-reply",
233            post(routes::discovery::queue_reply),
234        )
235        // Media — raise body limit for uploads (default 2MB is too small for images/video).
236        .route(
237            "/media/upload",
238            post(routes::media::upload).layer(DefaultBodyLimit::max(520 * 1024 * 1024)),
239        )
240        .route("/media/file", get(routes::media::serve_file))
241        // LAN settings
242        .route(
243            "/settings/lan",
244            get(routes::lan::get_status).patch(routes::lan::toggle_lan),
245        )
246        .route(
247            "/settings/lan/reset-passphrase",
248            post(routes::lan::reset_passphrase),
249        )
250        // Settings
251        .route("/settings/status", get(routes::settings::config_status))
252        .route("/settings/init", post(routes::settings::init_settings))
253        .route(
254            "/settings/validate",
255            post(routes::settings::validate_settings),
256        )
257        .route("/settings/defaults", get(routes::settings::get_defaults))
258        .route("/settings/test-llm", post(routes::settings::test_llm))
259        .route(
260            "/settings/factory-reset",
261            post(routes::settings::factory_reset),
262        )
263        .route(
264            "/settings/scraper-session",
265            get(routes::scraper_session::get_scraper_session)
266                .post(routes::scraper_session::import_scraper_session)
267                .delete(routes::scraper_session::delete_scraper_session),
268        )
269        .route(
270            "/settings",
271            get(routes::settings::get_settings).patch(routes::settings::patch_settings),
272        )
273        // Connectors
274        .route(
275            "/connectors/google-drive/link",
276            post(routes::connectors::link_google_drive),
277        )
278        .route(
279            "/connectors/google-drive/callback",
280            get(routes::connectors::callback_google_drive),
281        )
282        .route(
283            "/connectors/google-drive/status",
284            get(routes::connectors::status_google_drive),
285        )
286        .route(
287            "/connectors/google-drive/{id}",
288            delete(routes::connectors::disconnect_google_drive),
289        )
290        // MCP governance
291        .route(
292            "/mcp/policy",
293            get(routes::mcp::get_policy).patch(routes::mcp::patch_policy),
294        )
295        .route("/mcp/policy/templates", get(routes::mcp::list_templates))
296        .route(
297            "/mcp/policy/templates/{name}",
298            post(routes::mcp::apply_template),
299        )
300        .route(
301            "/mcp/telemetry/summary",
302            get(routes::mcp::telemetry_summary),
303        )
304        .route(
305            "/mcp/telemetry/metrics",
306            get(routes::mcp::telemetry_metrics),
307        )
308        .route("/mcp/telemetry/errors", get(routes::mcp::telemetry_errors))
309        .route("/mcp/telemetry/recent", get(routes::mcp::telemetry_recent))
310        // Runtime
311        .route("/runtime/status", get(routes::runtime::status))
312        .route("/runtime/start", post(routes::runtime::start))
313        .route("/runtime/stop", post(routes::runtime::stop))
314        // Onboarding OAuth (pre-account, auth-exempt)
315        .route(
316            "/onboarding/x-auth/start",
317            post(routes::onboarding::start_onboarding_auth),
318        )
319        .route(
320            "/onboarding/x-auth/callback",
321            post(routes::onboarding::complete_onboarding_auth),
322        )
323        .route(
324            "/onboarding/x-auth/status",
325            get(routes::onboarding::onboarding_auth_status),
326        )
327        .route(
328            "/onboarding/analyze-profile",
329            post(routes::onboarding::analyze_profile),
330        )
331        // Accounts
332        .route(
333            "/accounts",
334            get(routes::accounts::list_accounts).post(routes::accounts::create_account),
335        )
336        .route(
337            "/accounts/{id}/roles",
338            get(routes::accounts::list_roles)
339                .post(routes::accounts::set_role)
340                .delete(routes::accounts::remove_role),
341        )
342        .route(
343            "/accounts/{id}/sync-profile",
344            post(routes::accounts::sync_profile),
345        )
346        // X credential linking (before catch-all /accounts/{id})
347        .route(
348            "/accounts/{id}/x-auth/start",
349            post(routes::x_auth::start_link),
350        )
351        .route(
352            "/accounts/{id}/x-auth/callback",
353            post(routes::x_auth::complete_link),
354        )
355        .route(
356            "/accounts/{id}/x-auth/status",
357            get(routes::x_auth::link_status),
358        )
359        .route(
360            "/accounts/{id}/x-auth/tokens",
361            delete(routes::x_auth::unlink),
362        )
363        .route(
364            "/accounts/{id}",
365            get(routes::accounts::get_account)
366                .patch(routes::accounts::update_account)
367                .delete(routes::accounts::delete_account),
368        )
369        // WebSocket
370        .route("/ws", get(ws::ws_handler))
371        // Auth middleware — applied to all routes; exempt paths handled internally.
372        .layer(middleware::from_fn_with_state(
373            state.clone(),
374            auth::auth_middleware,
375        ));
376
377    Router::new()
378        .nest("/api", api)
379        .fallback(dashboard::serve_dashboard)
380        .layer(CorsLayer::permissive())
381        .layer(TraceLayer::new_for_http())
382        .with_state(state)
383}