1use std::sync::Arc;
7
8use axum::response::IntoResponse;
9use axum::routing::{get, post};
10use axum::Router;
11use futu_auth::{KeyStore, RuntimeCounters};
12use tower_http::cors::{Any, CorsLayer};
13
14use futu_server::router::RequestRouter;
15
16use crate::adapter::RestState;
17use crate::auth::{bearer_auth, AuthState};
18use crate::routes::{qot, sys, trd};
19use crate::ws::{self, WsBroadcaster};
20
21async fn metrics_handler() -> impl IntoResponse {
26 let body = futu_auth::metrics::global()
27 .map(|r| r.render_prometheus())
28 .unwrap_or_default();
29 (
30 axum::http::StatusCode::OK,
31 [(
32 axum::http::header::CONTENT_TYPE,
33 "text/plain; version=0.0.4",
34 )],
35 body,
36 )
37}
38
39async fn health_handler() -> impl IntoResponse {
45 (axum::http::StatusCode::OK, "ok")
46}
47
48pub fn build_router(router: Arc<RequestRouter>, ws_broadcaster: Arc<WsBroadcaster>) -> Router {
50 build_router_with_auth(
51 router,
52 ws_broadcaster,
53 Arc::new(KeyStore::empty()),
54 Arc::new(RuntimeCounters::new()),
55 )
56}
57
58pub fn build_router_with_auth(
64 router: Arc<RequestRouter>,
65 ws_broadcaster: Arc<WsBroadcaster>,
66 key_store: Arc<KeyStore>,
67 counters: Arc<RuntimeCounters>,
68) -> Router {
69 let state = RestState::with_auth(
70 router,
71 ws_broadcaster,
72 Arc::clone(&key_store),
73 Arc::clone(&counters),
74 );
75 let auth_state = AuthState::new(Arc::clone(&key_store), Arc::clone(&counters));
76
77 let cors = CorsLayer::new()
78 .allow_origin(Any)
79 .allow_methods(Any)
80 .allow_headers(Any);
81
82 Router::new()
83 .route("/ws", get(ws::ws_handler))
85 .route("/api/global-state", get(sys::get_global_state))
87 .route("/api/user-info", get(sys::get_user_info))
88 .route("/api/delay-statistics", get(sys::get_delay_statistics))
89 .route("/api/subscribe", post(qot::subscribe))
91 .route("/api/sub-info", get(qot::get_sub_info))
92 .route("/api/quote", post(qot::get_basic_qot))
93 .route("/api/kline", post(qot::get_kl))
94 .route("/api/orderbook", post(qot::get_order_book))
95 .route("/api/broker", post(qot::get_broker))
96 .route("/api/ticker", post(qot::get_ticker))
97 .route("/api/rt", post(qot::get_rt))
98 .route("/api/snapshot", post(qot::get_snapshot))
99 .route("/api/static-info", post(qot::get_static_info))
100 .route("/api/plate-set", post(qot::get_plate_set))
101 .route("/api/plate-security", post(qot::get_plate_security))
102 .route("/api/reference", post(qot::get_reference))
103 .route("/api/owner-plate", post(qot::get_owner_plate))
104 .route("/api/option-chain", post(qot::get_option_chain))
105 .route("/api/warrant", post(qot::get_warrant))
106 .route("/api/capital-flow", post(qot::get_capital_flow))
107 .route(
108 "/api/capital-distribution",
109 post(qot::get_capital_distribution),
110 )
111 .route("/api/user-security", post(qot::get_user_security))
112 .route("/api/stock-filter", post(qot::stock_filter))
113 .route("/api/ipo-list", post(qot::get_ipo_list))
114 .route("/api/future-info", post(qot::get_future_info))
115 .route("/api/market-state", post(qot::get_market_state))
116 .route("/api/history-kline", post(qot::request_history_kl))
117 .route("/api/accounts", get(trd::get_acc_list))
119 .route("/api/unlock-trade", post(trd::unlock_trade))
120 .route("/api/sub-acc-push", post(trd::sub_acc_push))
121 .route("/api/funds", post(trd::get_funds))
122 .route("/api/positions", post(trd::get_positions))
123 .route("/api/orders", post(trd::get_orders))
124 .route("/api/order", post(trd::place_order))
125 .route("/api/modify-order", post(trd::modify_order))
126 .route("/api/order-fills", post(trd::get_order_fills))
127 .route("/api/max-trd-qtys", post(trd::get_max_trd_qtys))
128 .route("/api/history-orders", post(trd::get_history_orders))
129 .route(
130 "/api/history-order-fills",
131 post(trd::get_history_order_fills),
132 )
133 .route("/api/margin-ratio", post(trd::get_margin_ratio))
134 .route("/api/order-fee", post(trd::get_order_fee))
135 .layer(axum::middleware::from_fn_with_state(
136 auth_state,
137 bearer_auth,
138 ))
139 .route("/metrics", get(metrics_handler))
143 .route("/health", get(health_handler))
144 .layer(cors)
145 .with_state(state)
146}
147
148pub async fn start(listen_addr: &str, router: Arc<RequestRouter>) -> std::io::Result<()> {
150 let ws_broadcaster = Arc::new(WsBroadcaster::new(1024));
151 let app = build_router(router, ws_broadcaster);
152 let listener = tokio::net::TcpListener::bind(listen_addr).await?;
153 tracing::info!(addr = %listen_addr, "REST API 服务已启动 (WebSocket: /ws)");
154 axum::serve(listener, app).await
155}
156
157pub async fn start_with_broadcaster(
159 listen_addr: &str,
160 router: Arc<RequestRouter>,
161 ws_broadcaster: Arc<WsBroadcaster>,
162) -> std::io::Result<()> {
163 let app = build_router(router, ws_broadcaster);
164 let listener = tokio::net::TcpListener::bind(listen_addr).await?;
165 tracing::info!(addr = %listen_addr, "REST API 服务已启动 (WebSocket: /ws)");
166 axum::serve(listener, app).await
167}
168
169pub async fn start_with_auth(
172 listen_addr: &str,
173 router: Arc<RequestRouter>,
174 ws_broadcaster: Arc<WsBroadcaster>,
175 key_store: Arc<KeyStore>,
176 counters: Arc<RuntimeCounters>,
177) -> std::io::Result<()> {
178 let scope_mode = key_store.is_configured();
179 let app = build_router_with_auth(router, ws_broadcaster, key_store, counters);
180 let listener = tokio::net::TcpListener::bind(listen_addr).await?;
181 tracing::info!(
182 addr = %listen_addr,
183 scope_mode,
184 "REST API 服务已启动 (WebSocket: /ws)"
185 );
186 if !scope_mode {
187 tracing::warn!("REST API started WITHOUT auth; all clients can access /api/* freely");
188 }
189 axum::serve(listener, app).await
190}