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::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
29async fn login_form(State(_state): State<UiState<DynDashboardApi>>) -> impl IntoResponse {
31 login_form_response("Fedimint Guardian Login")
32}
33
34async 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
50async 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
72async 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 @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 @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 @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 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 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 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 app.with_state(UiState::new(api))
211}