Skip to main content

fedimint_server_ui/dashboard/
mod.rs

1pub 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
35// Dashboard login form handler
36async 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
50// Dashboard login submit handler. Only mounted when the guardian has a
51// password configured, so `auth()` is always `Some` here.
52async 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
69// Download backup handler
70async 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
90// Prometheus metrics handler
91async 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
110// Main dashboard view
111async 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        // Conditionally add Lightning V2 UI if the module is available
155        @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        // Conditionally add Wallet V2 UI if the module is available
164        @if let Some(walletv2_module) = state.api.get_module::<fedimint_walletv2_server::Wallet>() {
165            (walletv2::render(walletv2_module).await)
166        }
167
168        // Conditionally add Mint V2 UI if the module is available
169        @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        // Conditionally add Wallet UI if the module is available
178        @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        // Conditionally add Meta UI if the module is available
187        @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        // Guardian Backup
196        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    // routeradd LNv2 gateway routes if the module exists
244    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    // Only add Meta module routes if the module exists
254    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    // Finalize the router with state
265    app.with_state(UiState::new(api, requires_auth))
266}