1use crate::api::{Api, ApiError, RegisterBranch, TransView};
13use axum::extract::{Query, State};
14use axum::http::StatusCode;
15use axum::routing::{get, post};
16use axum::{Json, Router};
17use dtmrs_core::SagaStep;
18use serde::{Deserialize, Serialize};
19use std::collections::HashMap;
20
21#[derive(Clone)]
23pub struct App {
24 api: Api,
25}
26
27impl App {
28 pub fn new(api: Api) -> Self {
29 Self { api }
30 }
31}
32
33#[derive(Deserialize)]
34struct SubmitReq {
35 gid: String,
36 #[serde(default = "default_trans_type")]
37 trans_type: String,
38 #[serde(default)]
40 steps: Vec<SagaStep>,
41}
42
43#[derive(Deserialize)]
45struct PrepareReq {
46 gid: String,
47 trans_type: String,
48 #[serde(default)]
50 actions: Vec<String>,
51 #[serde(default)]
53 query_prepared: String,
54 #[serde(default)]
56 grace_secs: Option<i64>,
57}
58
59#[derive(Deserialize)]
61struct RegisterBranchReq {
62 gid: String,
63 branch_id: String,
64 #[serde(default)]
65 confirm: String,
66 #[serde(default)]
67 cancel: String,
68 #[serde(default)]
70 r#try: String,
71 #[serde(default)]
72 commit: String,
73 #[serde(default)]
74 rollback: String,
75}
76
77fn default_trans_type() -> String {
78 "saga".into()
79}
80
81#[derive(Serialize)]
82struct Reply {
83 dtm_result: &'static str,
84 #[serde(skip_serializing_if = "Option::is_none")]
85 message: Option<String>,
86}
87
88impl Reply {
89 fn ok() -> Json<Self> {
90 Json(Self {
91 dtm_result: "SUCCESS",
92 message: None,
93 })
94 }
95 fn err(m: impl Into<String>) -> Json<Self> {
96 Json(Self {
97 dtm_result: "FAILURE",
98 message: Some(m.into()),
99 })
100 }
101}
102
103fn http_err(e: ApiError) -> (StatusCode, Json<Reply>) {
108 let code = match &e {
109 ApiError::BadRequest(_) => StatusCode::BAD_REQUEST,
110 ApiError::NotFound(_) => StatusCode::NOT_FOUND,
111 ApiError::Conflict(_) => StatusCode::OK,
112 ApiError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
113 };
114 (code, Reply::err(e.message().to_string()))
115}
116
117fn http_result(r: Result<(), ApiError>) -> (StatusCode, Json<Reply>) {
118 match r {
119 Ok(()) => (StatusCode::OK, Reply::ok()),
120 Err(e) => http_err(e),
121 }
122}
123
124
125pub fn router(app: App) -> Router {
127 routes(app)
128}
129
130pub fn router_with_auth(
136 app: App,
137 auth: std::sync::Arc<crate::auth::Auth>,
138 store: dtmrs_store::Store,
139) -> Router {
140 use axum::middleware;
141 let auth_routes = Router::new()
144 .route(
145 "/login",
146 get(crate::auth::login_page).post(crate::auth::login_submit),
147 )
148 .route("/logout", post(crate::auth::logout))
149 .with_state(auth.clone());
150 let token_routes = Router::new()
151 .route("/api/admin/tokens", get(crate::auth::tokens_list))
152 .route("/api/admin/tokens/create", post(crate::auth::tokens_create))
153 .route("/api/admin/tokens/revoke", post(crate::auth::tokens_revoke))
154 .route("/api/admin/tokens/reveal", post(crate::auth::tokens_reveal))
155 .with_state((auth.clone(), store));
156 routes(app)
157 .merge(auth_routes)
158 .merge(token_routes)
159 .layer(middleware::from_fn_with_state(auth, crate::auth::guard))
160}
161
162fn routes(app: App) -> Router {
163 Router::new()
164 .route("/api/dtmsvr/newGid", get(new_gid))
165 .route("/api/dtmsvr/prepare", post(prepare))
166 .route("/api/dtmsvr/registerBranch", post(register_branch))
167 .route("/api/dtmsvr/submit", post(submit))
168 .route("/api/dtmsvr/abort", post(abort))
169 .route("/api/dtmsvr/retry", post(retry))
170 .route("/api/dtmsvr/query", get(query))
171 .route("/api/dtmsvr/all", get(all))
172 .route("/health", get(|| async { "ok" }))
173 .route("/", get(console))
176 .route("/console", get(console))
177 .with_state(app)
178}
179
180async fn new_gid(State(app): State<App>) -> Json<HashMap<&'static str, String>> {
181 Json(HashMap::from([("gid", app.api.new_gid())]))
182}
183
184async fn submit(State(app): State<App>, Json(req): Json<SubmitReq>) -> (StatusCode, Json<Reply>) {
185 http_result(app.api.submit(&req.gid, &req.trans_type, &req.steps).await)
186}
187
188async fn prepare(State(app): State<App>, Json(req): Json<PrepareReq>) -> (StatusCode, Json<Reply>) {
189 http_result(
190 app.api
191 .prepare(
192 &req.gid,
193 &req.trans_type,
194 &req.actions,
195 &req.query_prepared,
196 req.grace_secs,
197 )
198 .await,
199 )
200}
201
202async fn register_branch(
203 State(app): State<App>,
204 Json(req): Json<RegisterBranchReq>,
205) -> (StatusCode, Json<Reply>) {
206 http_result(
207 app.api
208 .register_branch(&RegisterBranch {
209 gid: req.gid,
210 branch_id: req.branch_id,
211 confirm: req.confirm,
212 cancel: req.cancel,
213 r#try: req.r#try,
214 commit: req.commit,
215 rollback: req.rollback,
216 })
217 .await,
218 )
219}
220
221#[derive(Deserialize)]
222struct GidQuery {
223 gid: String,
224}
225
226async fn abort(State(app): State<App>, Json(q): Json<GidQuery>) -> (StatusCode, Json<Reply>) {
227 http_result(app.api.abort(&q.gid).await)
228}
229
230async fn retry(State(app): State<App>, Json(q): Json<GidQuery>) -> (StatusCode, Json<Reply>) {
232 http_result(app.api.retry(&q.gid).await)
233}
234
235async fn console() -> axum::response::Html<&'static str> {
237 axum::response::Html(include_str!("console.html"))
238}
239
240async fn query(
241 State(app): State<App>,
242 Query(q): Query<GidQuery>,
243) -> Result<Json<TransView>, (StatusCode, Json<Reply>)> {
244 app.api.query(&q.gid).await.map(Json).map_err(http_err)
245}
246
247async fn all(State(app): State<App>) -> Json<Vec<TransView>> {
248 Json(app.api.list_recent(100).await)
249}