mod charging;
mod commands;
mod reservations;
mod stations;
mod tariffs;
mod tokens;
use axum::extract::{Request, State};
use axum::http::header;
use axum::middleware::{self, Next};
use axum::response::{IntoResponse, Response};
use axum::routing::{get, post, put};
use axum::{Json, Router};
use crate::error::AppError;
use crate::state::App;
pub fn router(app: App) -> Router {
let v1 = Router::new()
.route("/stations", get(stations::search).post(stations::create))
.route(
"/stations/{identity}",
get(stations::get_one)
.patch(stations::update)
.delete(stations::delete),
)
.route("/stations/{identity}/password", put(stations::set_password))
.route("/stations/{identity}/evses", get(stations::evses))
.route(
"/stations/{identity}/transactions",
get(stations::transactions),
)
.route("/stations/{identity}/events", get(stations::events))
.route("/stations/{identity}/ocpp/{action}", post(commands::raw))
.route(
"/stations/{identity}/commands/{command}",
post(commands::generic),
)
.route(
"/stations/{identity}/charging-profiles",
get(charging::list).post(charging::set),
)
.route(
"/stations/{identity}/charging-profiles/clear",
post(charging::clear),
)
.route(
"/stations/{identity}/charging-profiles/sync",
post(charging::sync),
)
.route(
"/stations/{identity}/charging-profiles/{profile_id}/dynamic-update",
post(charging::dynamic_update),
)
.route(
"/stations/{identity}/charging-limits",
get(charging::limits),
)
.route(
"/stations/{identity}/composite-schedule",
get(charging::composite),
)
.route(
"/stations/{identity}/reservations",
get(reservations::list).post(reservations::create),
)
.route(
"/stations/{identity}/reservations/{id}",
get(reservations::get).delete(reservations::cancel),
)
.route(
"/stations/{identity}/tariffs",
get(tariffs::station_list).post(tariffs::station_set),
)
.route(
"/stations/{identity}/tariffs/{tariff_id}",
axum::routing::delete(tariffs::station_clear),
)
.route("/tariffs", get(tariffs::list))
.route(
"/tariffs/{tariff_id}",
get(tariffs::get).put(tariffs::put).delete(tariffs::delete),
)
.route(
"/transactions/{id}/tariff",
post(tariffs::change_transaction),
)
.route("/transactions/{id}/cost", get(tariffs::transaction_cost))
.route("/transactions/{id}", get(stations::transaction))
.route("/connections", get(connections))
.route("/id-tokens", get(tokens::list).put(tokens::upsert))
.route(
"/id-tokens/{token_type}/{id_token}",
axum::routing::delete(tokens::delete),
)
.layer(middleware::from_fn_with_state(app.clone(), require_token));
Router::new()
.route("/health", get(health))
.nest("/api/v1", v1)
.with_state(app)
}
async fn require_token(State(app): State<App>, req: Request, next: Next) -> Response {
if let Some(expected) = &app.cfg.api_token {
let ok = req
.headers()
.get(header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "))
.is_some_and(|t| constant_time_eq(t.trim().as_bytes(), expected.as_bytes()));
if !ok {
return AppError::Unauthorized.into_response();
}
}
next.run(req).await
}
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
a.len() == b.len() && a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
}
async fn health(State(app): State<App>) -> Response {
let db_ok = sqlx::query("SELECT 1").execute(&app.db).await.is_ok();
let body = serde_json::json!({
"status": if db_ok { "ok" } else { "degraded" },
"node": app.cfg.node_id,
"connections": app.hub.len(),
"database": db_ok,
});
let status = if db_ok {
axum::http::StatusCode::OK
} else {
axum::http::StatusCode::SERVICE_UNAVAILABLE
};
(status, Json(body)).into_response()
}
async fn connections(State(app): State<App>) -> Json<serde_json::Value> {
Json(serde_json::json!({ "node": app.cfg.node_id, "connections": app.hub.list() }))
}