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        .route(
48            "/analytics/engagement-rate",
49            get(routes::analytics::engagement_rate),
50        )
51        .route("/analytics/reach", get(routes::analytics::reach))
52        .route(
53            "/analytics/follower-growth",
54            get(routes::analytics::follower_growth),
55        )
56        .route("/analytics/best-times", get(routes::analytics::best_times))
57        .route("/analytics/heatmap", get(routes::analytics::heatmap))
58        .route(
59            "/analytics/content-breakdown",
60            get(routes::analytics::content_breakdown),
61        )
62        // Approval
63        .route("/approval/export", get(routes::approval::export_items))
64        .route("/approval", get(routes::approval::list_items))
65        .route("/approval/stats", get(routes::approval::stats))
66        .route("/approval/approve-all", post(routes::approval::approve_all))
67        .route(
68            "/approval/bulk/approve",
69            post(routes::approval::bulk_approve),
70        )
71        .route("/approval/bulk/reject", post(routes::approval::bulk_reject))
72        .route(
73            "/approval/{id}/history",
74            get(routes::approval::get_edit_history),
75        )
76        .route("/approval/{id}", patch(routes::approval::edit_item))
77        .route(
78            "/approval/{id}/approve",
79            post(routes::approval::approve_item),
80        )
81        .route("/approval/{id}/reject", post(routes::approval::reject_item))
82        // Activity
83        .route("/activity/export", get(routes::activity::export_activity))
84        .route("/activity", get(routes::activity::list_activity))
85        .route(
86            "/activity/rate-limits",
87            get(routes::activity::rate_limit_usage),
88        )
89        // Replies
90        .route("/replies", get(routes::replies::list_replies))
91        // Content
92        .route(
93            "/content/tweets",
94            get(routes::content::list_tweets).post(routes::content::compose_tweet),
95        )
96        .route(
97            "/content/threads",
98            get(routes::content::list_threads).post(routes::content::compose_thread),
99        )
100        .route("/content/calendar", get(routes::content::calendar))
101        .route("/content/schedule", get(routes::content::schedule))
102        .route("/content/compose", post(routes::content::compose))
103        .route(
104            "/content/scheduled/{id}",
105            patch(routes::content::edit_scheduled).delete(routes::content::cancel_scheduled),
106        )
107        // Draft Studio — Tags (literal paths before parameterized)
108        .route(
109            "/tags",
110            get(routes::content::list_account_tags).post(routes::content::create_account_tag),
111        )
112        // Draft Studio (new canonical paths)
113        .route(
114            "/drafts",
115            get(routes::content::list_studio_drafts).post(routes::content::create_studio_draft),
116        )
117        .route(
118            "/drafts/{id}",
119            get(routes::content::get_studio_draft)
120                .patch(routes::content::autosave_draft)
121                .delete(routes::content::delete_draft),
122        )
123        .route(
124            "/drafts/{id}/meta",
125            patch(routes::content::patch_draft_meta),
126        )
127        .route(
128            "/drafts/{id}/schedule",
129            post(routes::content::schedule_studio_draft),
130        )
131        .route(
132            "/drafts/{id}/reschedule",
133            patch(routes::content::reschedule_studio_draft),
134        )
135        .route(
136            "/drafts/{id}/unschedule",
137            post(routes::content::unschedule_studio_draft),
138        )
139        .route(
140            "/drafts/{id}/archive",
141            post(routes::content::archive_studio_draft),
142        )
143        .route(
144            "/drafts/{id}/restore",
145            post(routes::content::restore_studio_draft),
146        )
147        .route(
148            "/drafts/{id}/duplicate",
149            post(routes::content::duplicate_studio_draft),
150        )
151        .route(
152            "/drafts/{id}/revisions",
153            get(routes::content::list_draft_revisions).post(routes::content::create_draft_revision),
154        )
155        .route(
156            "/drafts/{id}/revisions/{rev_id}/restore",
157            post(routes::content::restore_from_revision),
158        )
159        .route(
160            "/drafts/{id}/activity",
161            get(routes::content::list_draft_activity),
162        )
163        .route(
164            "/drafts/{id}/provenance",
165            get(routes::content::get_draft_provenance),
166        )
167        .route("/drafts/{id}/tags", get(routes::content::list_draft_tags))
168        .route(
169            "/drafts/{id}/tags/{tag_id}",
170            post(routes::content::assign_draft_tag).delete(routes::content::unassign_draft_tag),
171        )
172        // Legacy drafts (backward compat)
173        .route(
174            "/content/drafts",
175            get(routes::content::list_drafts).post(routes::content::create_draft),
176        )
177        .route(
178            "/content/drafts/{id}",
179            patch(routes::content::edit_draft).delete(routes::content::delete_draft),
180        )
181        .route(
182            "/content/drafts/{id}/schedule",
183            post(routes::content::schedule_draft),
184        )
185        .route(
186            "/content/drafts/{id}/publish",
187            post(routes::content::publish_draft),
188        )
189        .route(
190            "/content/drafts/{id}/provenance",
191            get(routes::content::get_draft_provenance),
192        )
193        // Ingest
194        .route("/ingest", post(routes::ingest::ingest))
195        // Sources
196        .route("/sources/status", get(routes::sources::source_status))
197        .route(
198            "/sources/{id}/reindex",
199            post(routes::sources::reindex_source),
200        )
201        // Targets
202        .route(
203            "/targets",
204            get(routes::targets::list_targets).post(routes::targets::add_target),
205        )
206        .route(
207            "/targets/{username}/timeline",
208            get(routes::targets::target_timeline),
209        )
210        .route(
211            "/targets/{username}/stats",
212            get(routes::targets::target_stats),
213        )
214        .route(
215            "/targets/{username}",
216            delete(routes::targets::remove_target),
217        )
218        // Strategy
219        .route("/strategy/current", get(routes::strategy::current))
220        .route("/strategy/history", get(routes::strategy::history))
221        .route("/strategy/refresh", post(routes::strategy::refresh))
222        .route("/strategy/inputs", get(routes::strategy::inputs))
223        // Costs — LLM
224        .route("/costs/summary", get(routes::costs::summary))
225        .route("/costs/daily", get(routes::costs::daily))
226        .route("/costs/by-model", get(routes::costs::by_model))
227        .route("/costs/by-type", get(routes::costs::by_type))
228        // Costs — X API
229        .route("/costs/x-api/summary", get(routes::costs::x_api_summary))
230        .route("/costs/x-api/daily", get(routes::costs::x_api_daily))
231        .route(
232            "/costs/x-api/by-endpoint",
233            get(routes::costs::x_api_by_endpoint),
234        )
235        // AI Assist
236        .route("/assist/tweet", post(routes::assist::assist_tweet))
237        .route("/assist/reply", post(routes::assist::assist_reply))
238        .route("/assist/thread", post(routes::assist::assist_thread))
239        .route("/assist/improve", post(routes::assist::assist_improve))
240        .route(
241            "/assist/highlights",
242            post(routes::assist::assist_highlights),
243        )
244        .route("/assist/hooks", post(routes::assist::hooks::assist_hooks))
245        .route(
246            "/assist/angles",
247            post(routes::assist::angles::assist_angles),
248        )
249        .route("/assist/topics", get(routes::assist::assist_topics))
250        .route(
251            "/assist/optimal-times",
252            get(routes::assist::assist_optimal_times),
253        )
254        .route("/assist/mode", get(routes::assist::get_mode))
255        // Vault
256        .route(
257            "/vault/evidence",
258            get(routes::vault::evidence::search_evidence),
259        )
260        .route(
261            "/vault/index-status",
262            get(routes::vault::index_status::get_index_status),
263        )
264        .route("/vault/sources", get(routes::vault::vault_sources))
265        .route("/vault/notes", get(routes::vault::search_notes))
266        .route(
267            "/vault/notes/{id}/neighbors",
268            get(routes::vault::note_neighbors),
269        )
270        .route("/vault/notes/{id}", get(routes::vault::note_detail))
271        .route("/vault/search", get(routes::vault::search_fragments))
272        .route("/vault/resolve-refs", post(routes::vault::resolve_refs))
273        .route(
274            "/vault/send-selection",
275            post(routes::vault::selections::send_selection),
276        )
277        .route(
278            "/vault/selection/{session_id}",
279            get(routes::vault::selections::get_selection),
280        )
281        // Discovery feed
282        .route("/discovery/feed", get(routes::discovery::feed))
283        .route("/discovery/keywords", get(routes::discovery::keywords))
284        .route(
285            "/discovery/{tweet_id}/compose-reply",
286            post(routes::discovery::compose_reply),
287        )
288        .route(
289            "/discovery/{tweet_id}/queue-reply",
290            post(routes::discovery::queue_reply),
291        )
292        // Media — raise body limit for uploads (default 2MB is too small for images/video).
293        .route(
294            "/media/upload",
295            post(routes::media::upload).layer(DefaultBodyLimit::max(520 * 1024 * 1024)),
296        )
297        .route("/media/file", get(routes::media::serve_file))
298        // LAN settings
299        .route(
300            "/settings/lan",
301            get(routes::lan::get_status).patch(routes::lan::toggle_lan),
302        )
303        .route(
304            "/settings/lan/reset-passphrase",
305            post(routes::lan::reset_passphrase),
306        )
307        // Settings
308        .route("/settings/status", get(routes::settings::config_status))
309        .route("/settings/init", post(routes::settings::init_settings))
310        .route(
311            "/settings/validate",
312            post(routes::settings::validate_settings),
313        )
314        .route("/settings/defaults", get(routes::settings::get_defaults))
315        .route("/settings/test-llm", post(routes::settings::test_llm))
316        .route(
317            "/settings/factory-reset",
318            post(routes::settings::factory_reset),
319        )
320        .route(
321            "/settings/scraper-session",
322            get(routes::scraper_session::get_scraper_session)
323                .post(routes::scraper_session::import_scraper_session)
324                .delete(routes::scraper_session::delete_scraper_session),
325        )
326        .route(
327            "/settings",
328            get(routes::settings::get_settings).patch(routes::settings::patch_settings),
329        )
330        // Connectors
331        .route(
332            "/connectors/google-drive/link",
333            post(routes::connectors::link_google_drive),
334        )
335        .route(
336            "/connectors/google-drive/callback",
337            get(routes::connectors::callback_google_drive),
338        )
339        .route(
340            "/connectors/google-drive/status",
341            get(routes::connectors::status_google_drive),
342        )
343        .route(
344            "/connectors/google-drive/{id}",
345            delete(routes::connectors::disconnect_google_drive),
346        )
347        // MCP governance
348        .route(
349            "/mcp/policy",
350            get(routes::mcp::get_policy).patch(routes::mcp::patch_policy),
351        )
352        .route("/mcp/policy/templates", get(routes::mcp::list_templates))
353        .route(
354            "/mcp/policy/templates/{name}",
355            post(routes::mcp::apply_template),
356        )
357        .route(
358            "/mcp/telemetry/summary",
359            get(routes::mcp::telemetry_summary),
360        )
361        .route(
362            "/mcp/telemetry/metrics",
363            get(routes::mcp::telemetry_metrics),
364        )
365        .route("/mcp/telemetry/errors", get(routes::mcp::telemetry_errors))
366        .route("/mcp/telemetry/recent", get(routes::mcp::telemetry_recent))
367        // Runtime
368        .route("/runtime/status", get(routes::runtime::status))
369        .route("/runtime/start", post(routes::runtime::start))
370        .route("/runtime/stop", post(routes::runtime::stop))
371        // Onboarding OAuth (pre-account, auth-exempt)
372        .route(
373            "/onboarding/x-auth/start",
374            post(routes::onboarding::start_onboarding_auth),
375        )
376        .route(
377            "/onboarding/x-auth/callback",
378            post(routes::onboarding::complete_onboarding_auth),
379        )
380        .route(
381            "/onboarding/x-auth/status",
382            get(routes::onboarding::onboarding_auth_status),
383        )
384        .route(
385            "/onboarding/analyze-profile",
386            post(routes::onboarding::analyze_profile),
387        )
388        // Accounts
389        .route(
390            "/accounts",
391            get(routes::accounts::list_accounts).post(routes::accounts::create_account),
392        )
393        .route(
394            "/accounts/{id}/roles",
395            get(routes::accounts::list_roles)
396                .post(routes::accounts::set_role)
397                .delete(routes::accounts::remove_role),
398        )
399        .route(
400            "/accounts/{id}/sync-profile",
401            post(routes::accounts::sync_profile),
402        )
403        // X credential linking (before catch-all /accounts/{id})
404        .route(
405            "/accounts/{id}/x-auth/start",
406            post(routes::x_auth::start_link),
407        )
408        .route(
409            "/accounts/{id}/x-auth/callback",
410            post(routes::x_auth::complete_link),
411        )
412        .route(
413            "/accounts/{id}/x-auth/status",
414            get(routes::x_auth::link_status),
415        )
416        .route(
417            "/accounts/{id}/x-auth/tokens",
418            delete(routes::x_auth::unlink),
419        )
420        .route(
421            "/accounts/{id}",
422            get(routes::accounts::get_account)
423                .patch(routes::accounts::update_account)
424                .delete(routes::accounts::delete_account),
425        )
426        // Telemetry
427        .route("/telemetry/events", post(routes::telemetry::ingest_events))
428        // WebSocket
429        .route("/ws", get(ws::ws_handler))
430        // Auth middleware — applied to all routes; exempt paths handled internally.
431        .layer(middleware::from_fn_with_state(
432            state.clone(),
433            auth::auth_middleware,
434        ));
435
436    Router::new()
437        .nest("/api", api)
438        .fallback(dashboard::serve_dashboard)
439        .layer(CorsLayer::permissive())
440        .layer(TraceLayer::new_for_http())
441        .with_state(state)
442}