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