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