1mod charging;
7mod commands;
8mod reservations;
9mod stations;
10mod tariffs;
11mod tokens;
12
13use axum::extract::{Request, State};
14use axum::http::header;
15use axum::middleware::{self, Next};
16use axum::response::{IntoResponse, Response};
17use axum::routing::{get, post, put};
18use axum::{Json, Router};
19
20use crate::error::AppError;
21use crate::state::App;
22
23pub fn router(app: App) -> Router {
24 let v1 = Router::new()
25 .route("/stations", get(stations::search).post(stations::create))
26 .route(
27 "/stations/{identity}",
28 get(stations::get_one)
29 .patch(stations::update)
30 .delete(stations::delete),
31 )
32 .route("/stations/{identity}/password", put(stations::set_password))
33 .route("/stations/{identity}/evses", get(stations::evses))
34 .route(
35 "/stations/{identity}/transactions",
36 get(stations::transactions),
37 )
38 .route("/stations/{identity}/events", get(stations::events))
39 .route("/stations/{identity}/ocpp/{action}", post(commands::raw))
40 .route(
41 "/stations/{identity}/commands/{command}",
42 post(commands::generic),
43 )
44 .route(
45 "/stations/{identity}/charging-profiles",
46 get(charging::list).post(charging::set),
47 )
48 .route(
49 "/stations/{identity}/charging-profiles/clear",
50 post(charging::clear),
51 )
52 .route(
53 "/stations/{identity}/charging-profiles/sync",
54 post(charging::sync),
55 )
56 .route(
57 "/stations/{identity}/charging-profiles/{profile_id}/dynamic-update",
58 post(charging::dynamic_update),
59 )
60 .route(
61 "/stations/{identity}/charging-limits",
62 get(charging::limits),
63 )
64 .route(
65 "/stations/{identity}/composite-schedule",
66 get(charging::composite),
67 )
68 .route(
69 "/stations/{identity}/reservations",
70 get(reservations::list).post(reservations::create),
71 )
72 .route(
73 "/stations/{identity}/reservations/{id}",
74 get(reservations::get).delete(reservations::cancel),
75 )
76 .route(
77 "/stations/{identity}/tariffs",
78 get(tariffs::station_list).post(tariffs::station_set),
79 )
80 .route(
81 "/stations/{identity}/tariffs/{tariff_id}",
82 axum::routing::delete(tariffs::station_clear),
83 )
84 .route("/tariffs", get(tariffs::list))
85 .route(
86 "/tariffs/{tariff_id}",
87 get(tariffs::get).put(tariffs::put).delete(tariffs::delete),
88 )
89 .route(
90 "/transactions/{id}/tariff",
91 post(tariffs::change_transaction),
92 )
93 .route("/transactions/{id}/cost", get(tariffs::transaction_cost))
94 .route("/transactions/{id}", get(stations::transaction))
95 .route("/connections", get(connections))
96 .route("/id-tokens", get(tokens::list).put(tokens::upsert))
97 .route(
98 "/id-tokens/{token_type}/{id_token}",
99 axum::routing::delete(tokens::delete),
100 )
101 .layer(middleware::from_fn_with_state(app.clone(), require_token));
102 Router::new()
103 .route("/health", get(health))
104 .nest("/api/v1", v1)
105 .with_state(app)
106}
107
108async fn require_token(State(app): State<App>, req: Request, next: Next) -> Response {
109 if let Some(expected) = &app.cfg.api_token {
110 let ok = req
111 .headers()
112 .get(header::AUTHORIZATION)
113 .and_then(|v| v.to_str().ok())
114 .and_then(|v| v.strip_prefix("Bearer "))
115 .is_some_and(|t| constant_time_eq(t.trim().as_bytes(), expected.as_bytes()));
116 if !ok {
117 return AppError::Unauthorized.into_response();
118 }
119 }
120 next.run(req).await
121}
122
123fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
124 a.len() == b.len() && a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
125}
126
127async fn health(State(app): State<App>) -> Response {
128 let db_ok = sqlx::query("SELECT 1").execute(&app.db).await.is_ok();
129 let body = serde_json::json!({
130 "status": if db_ok { "ok" } else { "degraded" },
131 "node": app.cfg.node_id,
132 "connections": app.hub.len(),
133 "database": db_ok,
134 });
135 let status = if db_ok {
136 axum::http::StatusCode::OK
137 } else {
138 axum::http::StatusCode::SERVICE_UNAVAILABLE
139 };
140 (status, Json(body)).into_response()
141}
142
143async fn connections(State(app): State<App>) -> Json<serde_json::Value> {
144 Json(serde_json::json!({ "node": app.cfg.node_id, "connections": app.hub.list() }))
145}