fedimint_server_ui/dashboard/
mod.rs1pub mod audit;
2pub mod bitcoin;
3pub(crate) mod consensus_explorer;
4pub mod general;
5pub mod invite;
6pub mod latency;
7pub mod modules;
8
9use axum::Router;
10use axum::body::Body;
11use axum::extract::{Form, State};
12use axum::http::{StatusCode, header};
13use axum::response::{Html, IntoResponse, Response};
14use axum::routing::{get, post};
15use axum_extra::extract::cookie::CookieJar;
16use consensus_explorer::consensus_explorer_view;
17use fedimint_metrics::{Encoder, REGISTRY, TextEncoder};
18use fedimint_server_core::dashboard_ui::{DashboardApiModuleExt, DynDashboardApi};
19use fedimint_ui_common::assets::WithStaticRoutesExt;
20use fedimint_ui_common::auth::UserAuth;
21use fedimint_ui_common::{
22 CONNECTIVITY_CHECK_ROUTE, LOGIN_ROUTE, LoginInput, ROOT_ROUTE, UiState,
23 connectivity_check_handler, dashboard_layout, login_form, login_submit_response,
24 single_card_layout_with_version,
25};
26use maud::html;
27use {
28 fedimint_lnv2_server, fedimint_meta_server, fedimint_mintv2_server, fedimint_wallet_server,
29 fedimint_walletv2_server,
30};
31
32use crate::dashboard::modules::{lnv2, meta, mintv2, wallet, walletv2};
33use crate::{DOWNLOAD_BACKUP_ROUTE, EXPLORER_IDX_ROUTE, EXPLORER_ROUTE, METRICS_ROUTE};
34
35async fn login_form_handler(State(state): State<UiState<DynDashboardApi>>) -> impl IntoResponse {
37 let version = state.api.fedimintd_version().await;
38 let version_hash = state.api.fedimintd_version_hash().await;
39 Html(
40 single_card_layout_with_version(
41 "Enter Password",
42 login_form(None),
43 &version,
44 version_hash.as_deref(),
45 )
46 .into_string(),
47 )
48}
49
50async fn login_submit(
53 State(state): State<UiState<DynDashboardApi>>,
54 jar: CookieJar,
55 Form(input): Form<LoginInput>,
56) -> impl IntoResponse {
57 login_submit_response(
58 state
59 .api
60 .auth_ui()
61 .expect("login route is mounted only when auth is configured"),
62 state.auth_cookie_name,
63 state.auth_cookie_value,
64 jar,
65 input,
66 )
67}
68
69async fn download_backup(
71 State(state): State<UiState<DynDashboardApi>>,
72 user_auth: UserAuth,
73) -> impl IntoResponse {
74 let backup = state
75 .api
76 .download_guardian_config_backup(&user_auth.guardian_auth_token)
77 .await;
78 let filename = "guardian-backup.tar";
79
80 Response::builder()
81 .header(header::CONTENT_TYPE, "application/x-tar")
82 .header(
83 header::CONTENT_DISPOSITION,
84 format!("attachment; filename=\"{filename}\""),
85 )
86 .body(Body::from(backup.tar_archive_bytes))
87 .expect("Failed to build response")
88}
89
90async fn metrics_handler(_user_auth: UserAuth) -> impl IntoResponse {
92 let metric_families = REGISTRY.gather();
93 let result = || -> Result<String, Box<dyn std::error::Error>> {
94 let mut buffer = Vec::new();
95 let encoder = TextEncoder::new();
96 encoder.encode(&metric_families, &mut buffer)?;
97 Ok(String::from_utf8(buffer)?)
98 };
99 match result() {
100 Ok(metrics) => (
101 StatusCode::OK,
102 [(header::CONTENT_TYPE, "text/plain; charset=utf-8")],
103 metrics,
104 )
105 .into_response(),
106 Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")).into_response(),
107 }
108}
109
110async fn dashboard_view(
112 State(state): State<UiState<DynDashboardApi>>,
113 _auth: UserAuth,
114) -> impl IntoResponse {
115 let guardian_names = state.api.guardian_names().await;
116 let federation_name = state.api.federation_name().await;
117 let session_count = state.api.session_count().await;
118 let fedimintd_version = state.api.fedimintd_version().await;
119 let fedimintd_version_hash = state.api.fedimintd_version_hash().await;
120 let consensus_ord_latency = state.api.consensus_ord_latency().await;
121 let p2p_connection_status = state.api.p2p_connection_status().await;
122 let invite_code = state.api.federation_invite_code().await;
123 let audit_summary = state.api.federation_audit().await;
124 let bitcoin_rpc_url = state.api.bitcoin_rpc_url().await;
125 let bitcoin_rpc_status = state.api.bitcoin_rpc_status().await;
126
127 let content = html! {
128 div class="row gy-4" {
129 div class="col-md-6" {
130 (general::render(&federation_name, session_count, &guardian_names))
131 }
132
133 div class="col-md-6" {
134 (invite::render(&invite_code, session_count))
135 }
136 }
137
138 div class="row gy-4 mt-2" {
139 div class="col-lg-6" {
140 (audit::render(&audit_summary))
141 }
142
143 div class="col-lg-6" {
144 (latency::render(consensus_ord_latency, &p2p_connection_status))
145 }
146 }
147
148 div class="row gy-4 mt-2" {
149 div class="col-12" {
150 (bitcoin::render(bitcoin_rpc_url, &bitcoin_rpc_status))
151 }
152 }
153
154 @if let Some(lightning) = state.api.get_module::<fedimint_lnv2_server::Lightning>() {
156 div class="row gy-4 mt-2" {
157 div class="col-12" {
158 (lnv2::render(lightning).await)
159 }
160 }
161 }
162
163 @if let Some(walletv2_module) = state.api.get_module::<fedimint_walletv2_server::Wallet>() {
165 (walletv2::render(walletv2_module).await)
166 }
167
168 @if let Some(mint_module) = state.api.get_module::<fedimint_mintv2_server::Mint>() {
170 div class="row gy-4 mt-2" {
171 div class="col-12" {
172 (mintv2::render(mint_module).await)
173 }
174 }
175 }
176
177 @if let Some(wallet_module) = state.api.get_module::<fedimint_wallet_server::Wallet>() {
179 div class="row gy-4 mt-2" {
180 div class="col-12" {
181 (wallet::render(wallet_module).await)
182 }
183 }
184 }
185
186 @if let Some(meta_module) = state.api.get_module::<fedimint_meta_server::Meta>() {
188 div class="row gy-4 mt-2" {
189 div class="col-12" {
190 (meta::render(meta_module).await)
191 }
192 }
193 }
194
195 div class="row gy-4 mt-2" {
197 div class="col-12" {
198 div class="card" {
199 div class="card-header dashboard-header" { "Guardian Backup" }
200 div class="card-body" {
201 div class="alert alert-warning mb-3" {
202 "You only need to download this backup once. Use it to restore your guardian if your server fails. Store this file securely since anyone with it can run your guardian node."
203 }
204 a href="/download-backup" class="btn btn-primary" {
205 "Download"
206 }
207 }
208 }
209 }
210 }
211 };
212
213 Html(
214 dashboard_layout(
215 content,
216 &fedimintd_version,
217 fedimintd_version_hash.as_deref(),
218 )
219 .into_string(),
220 )
221 .into_response()
222}
223
224pub fn router(api: DynDashboardApi) -> Router {
225 let requires_auth = api.auth_ui().is_some();
226
227 let mut app = Router::new()
228 .route(ROOT_ROUTE, get(dashboard_view))
229 .route(EXPLORER_ROUTE, get(consensus_explorer_view))
230 .route(EXPLORER_IDX_ROUTE, get(consensus_explorer_view))
231 .route(DOWNLOAD_BACKUP_ROUTE, get(download_backup))
232 .route(METRICS_ROUTE, get(metrics_handler))
233 .route(
234 CONNECTIVITY_CHECK_ROUTE,
235 get(connectivity_check_handler::<DynDashboardApi>),
236 )
237 .with_static_routes();
238
239 if requires_auth {
240 app = app.route(LOGIN_ROUTE, get(login_form_handler).post(login_submit));
241 }
242
243 if api
245 .get_module::<fedimint_lnv2_server::Lightning>()
246 .is_some()
247 {
248 app = app
249 .route(lnv2::LNV2_ADD_ROUTE, post(lnv2::post_add))
250 .route(lnv2::LNV2_REMOVE_ROUTE, post(lnv2::post_remove));
251 }
252
253 if api.get_module::<fedimint_meta_server::Meta>().is_some() {
255 app = app
256 .route(meta::META_SUBMIT_ROUTE, post(meta::post_submit))
257 .route(meta::META_SET_ROUTE, post(meta::post_set))
258 .route(meta::META_RESET_ROUTE, post(meta::post_reset))
259 .route(meta::META_DELETE_ROUTE, post(meta::post_delete))
260 .route(meta::META_MERGE_ROUTE, post(meta::post_merge))
261 .route(meta::META_VALUE_INPUT_ROUTE, get(meta::get_value_input));
262 }
263
264 app.with_state(UiState::new(api, requires_auth))
266}