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::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_server_core::dashboard_ui::{DashboardApiModuleExt, DynDashboardApi};
18use maud::{DOCTYPE, Markup, html};
19use {fedimint_lnv2_server, fedimint_meta_server, fedimint_wallet_server};
20
21use crate::assets::WithStaticRoutesExt as _;
22use crate::auth::UserAuth;
23use crate::dashboard::modules::{lnv2, meta, wallet};
24use crate::{
25    DOWNLOAD_BACKUP_ROUTE, EXPLORER_IDX_ROUTE, EXPLORER_ROUTE, LOGIN_ROUTE, LoginInput, ROOT_ROUTE,
26    UiState, common_head, login_form_response, login_submit_response,
27};
28
29pub fn dashboard_layout(content: Markup, fedimintd_version: Option<&str>) -> Markup {
30    html! {
31        (DOCTYPE)
32        html {
33            head {
34                (common_head("Dashboard"))
35            }
36            body {
37                div class="container" {
38                    header class="text-center mb-4" {
39                        h1 class="header-title mb-1" { "Fedimint Guardian UI" }
40                        @if let Some(version) = fedimintd_version {
41                            div {
42                                small class="text-muted" { "v" (version) }
43                            }
44                        }
45                    }
46
47                    (content)
48                }
49                script src="/assets/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous" {}
50            }
51        }
52    }
53}
54
55// Dashboard login form handler
56async fn login_form(State(_state): State<UiState<DynDashboardApi>>) -> impl IntoResponse {
57    login_form_response()
58}
59
60// Dashboard login submit handler
61async fn login_submit(
62    State(state): State<UiState<DynDashboardApi>>,
63    jar: CookieJar,
64    Form(input): Form<LoginInput>,
65) -> impl IntoResponse {
66    login_submit_response(
67        state.api.auth().await,
68        state.auth_cookie_name,
69        state.auth_cookie_value,
70        jar,
71        input,
72    )
73    .into_response()
74}
75
76// Download backup handler
77async fn download_backup(
78    State(state): State<UiState<DynDashboardApi>>,
79    user_auth: UserAuth,
80) -> impl IntoResponse {
81    let api_auth = state.api.auth().await;
82    let backup = state
83        .api
84        .download_guardian_config_backup(&api_auth.0, &user_auth.guardian_auth_token)
85        .await;
86    let filename = "guardian-backup.tar";
87
88    Response::builder()
89        .header(header::CONTENT_TYPE, "application/x-tar")
90        .header(
91            header::CONTENT_DISPOSITION,
92            format!("attachment; filename=\"{filename}\""),
93        )
94        .body(Body::from(backup.tar_archive_bytes))
95        .expect("Failed to build response")
96}
97
98// Main dashboard view
99async fn dashboard_view(
100    State(state): State<UiState<DynDashboardApi>>,
101    _auth: UserAuth,
102) -> impl IntoResponse {
103    let guardian_names = state.api.guardian_names().await;
104    let federation_name = state.api.federation_name().await;
105    let session_count = state.api.session_count().await;
106    let fedimintd_version = state.api.fedimintd_version().await;
107    let consensus_ord_latency = state.api.consensus_ord_latency().await;
108    let p2p_connection_status = state.api.p2p_connection_status().await;
109    let p2p_connection_type_status = state.api.p2p_connection_type_status().await;
110    let invite_code = state.api.federation_invite_code().await;
111    let audit_summary = state.api.federation_audit().await;
112    let bitcoin_rpc_url = state.api.bitcoin_rpc_url().await;
113    let bitcoin_rpc_status = state.api.bitcoin_rpc_status().await;
114
115    let content = html! {
116        div class="row gy-4" {
117            div class="col-md-6" {
118                (general::render(&federation_name, session_count, &guardian_names))
119            }
120
121            div class="col-md-6" {
122                (invite::render(&invite_code))
123            }
124        }
125
126        div class="row gy-4 mt-2" {
127            div class="col-lg-6" {
128                (audit::render(&audit_summary))
129            }
130
131            div class="col-lg-6" {
132                (latency::render(consensus_ord_latency, &p2p_connection_status, &p2p_connection_type_status))
133            }
134        }
135
136        div class="row gy-4 mt-2" {
137            div class="col-12" {
138                (bitcoin::render(bitcoin_rpc_url, &bitcoin_rpc_status))
139            }
140        }
141
142        // Conditionally add Lightning V2 UI if the module is available
143        @if let Some(lightning) = state.api.get_module::<fedimint_lnv2_server::Lightning>() {
144            div class="row gy-4 mt-2" {
145                div class="col-12" {
146                    (lnv2::render(lightning).await)
147                }
148            }
149        }
150
151        // Conditionally add Wallet UI if the module is available
152        @if let Some(wallet_module) = state.api.get_module::<fedimint_wallet_server::Wallet>() {
153            div class="row gy-4 mt-2" {
154                div class="col-12" {
155                    (wallet::render(wallet_module).await)
156                }
157            }
158        }
159
160        // Conditionally add Meta UI if the module is available
161        @if let Some(meta_module) = state.api.get_module::<fedimint_meta_server::Meta>() {
162            div class="row gy-4 mt-2" {
163                div class="col-12" {
164                    (meta::render(meta_module).await)
165                }
166            }
167        }
168
169        // Guardian Configuration Backup section
170        div class="row gy-4 mt-4" {
171            div class="col-12" {
172                div class="card" {
173                    div class="card-header bg-warning text-dark" {
174                        h5 class="mb-0" { "Guardian Configuration Backup" }
175                    }
176                    div class="card-body" {
177                        div class="row" {
178                            div class="col-lg-6 mb-3 mb-lg-0" {
179                                p {
180                                    "You only need to download this backup once."
181                                }
182                                p {
183                                    "Use it to restore your guardian if your server fails."
184                                }
185                                a href="/download-backup" class="btn btn-outline-warning btn-lg mt-2" {
186                                    "Download Guardian Backup"
187                                }
188                            }
189                            div class="col-lg-6" {
190                                div class="alert alert-warning mb-0" {
191                                    strong { "Security Warning" }
192                                    br;
193                                    "Store this file securely since anyone with it and your password can run your guardian node."
194                                }
195                            }
196                        }
197                    }
198                }
199            }
200        }
201    };
202
203    Html(dashboard_layout(content, Some(&fedimintd_version)).into_string()).into_response()
204}
205
206pub fn router(api: DynDashboardApi) -> Router {
207    let mut app = Router::new()
208        .route(ROOT_ROUTE, get(dashboard_view))
209        .route(LOGIN_ROUTE, get(login_form).post(login_submit))
210        .route(EXPLORER_ROUTE, get(consensus_explorer_view))
211        .route(EXPLORER_IDX_ROUTE, get(consensus_explorer_view))
212        .route(DOWNLOAD_BACKUP_ROUTE, get(download_backup))
213        .with_static_routes();
214
215    // routeradd LNv2 gateway routes if the module exists
216    if api
217        .get_module::<fedimint_lnv2_server::Lightning>()
218        .is_some()
219    {
220        app = app
221            .route(lnv2::LNV2_ADD_ROUTE, post(lnv2::post_add))
222            .route(lnv2::LNV2_REMOVE_ROUTE, post(lnv2::post_remove));
223    }
224
225    // Only add Meta module routes if the module exists
226    if api.get_module::<fedimint_meta_server::Meta>().is_some() {
227        app = app
228            .route(meta::META_SUBMIT_ROUTE, post(meta::post_submit))
229            .route(meta::META_SET_ROUTE, post(meta::post_set))
230            .route(meta::META_RESET_ROUTE, post(meta::post_reset))
231            .route(meta::META_DELETE_ROUTE, post(meta::post_delete));
232    }
233
234    // Finalize the router with state
235    app.with_state(UiState::new(api))
236}