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 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
55async fn login_form(State(_state): State<UiState<DynDashboardApi>>) -> impl IntoResponse {
57 login_form_response()
58}
59
60async 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
76async 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
98async 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 invite_code = state.api.federation_invite_code().await;
110 let audit_summary = state.api.federation_audit().await;
111 let bitcoin_rpc_url = state.api.bitcoin_rpc_url().await;
112 let bitcoin_rpc_status = state.api.bitcoin_rpc_status().await;
113
114 let content = html! {
115 div class="row gy-4" {
116 div class="col-md-6" {
117 (general::render(&federation_name, session_count, &guardian_names))
118 }
119
120 div class="col-md-6" {
121 (invite::render(&invite_code))
122 }
123 }
124
125 div class="row gy-4 mt-2" {
126 div class="col-lg-6" {
127 (audit::render(&audit_summary))
128 }
129
130 div class="col-lg-6" {
131 (latency::render(consensus_ord_latency, &p2p_connection_status))
132 }
133 }
134
135 div class="row gy-4 mt-2" {
136 div class="col-12" {
137 (bitcoin::render(bitcoin_rpc_url, &bitcoin_rpc_status))
138 }
139 }
140
141 @if let Some(lightning) = state.api.get_module::<fedimint_lnv2_server::Lightning>() {
143 div class="row gy-4 mt-2" {
144 div class="col-12" {
145 (lnv2::render(lightning).await)
146 }
147 }
148 }
149
150 @if let Some(wallet_module) = state.api.get_module::<fedimint_wallet_server::Wallet>() {
152 div class="row gy-4 mt-2" {
153 div class="col-12" {
154 (wallet::render(wallet_module).await)
155 }
156 }
157 }
158
159 @if let Some(meta_module) = state.api.get_module::<fedimint_meta_server::Meta>() {
161 div class="row gy-4 mt-2" {
162 div class="col-12" {
163 (meta::render(meta_module).await)
164 }
165 }
166 }
167
168 div class="row gy-4 mt-4" {
170 div class="col-12" {
171 div class="card" {
172 div class="card-header bg-warning text-dark" {
173 h5 class="mb-0" { "Guardian Configuration Backup" }
174 }
175 div class="card-body" {
176 div class="row" {
177 div class="col-lg-6 mb-3 mb-lg-0" {
178 p {
179 "You only need to download this backup once."
180 }
181 p {
182 "Use it to restore your guardian if your server fails."
183 }
184 a href="/download-backup" class="btn btn-outline-warning btn-lg mt-2" {
185 "Download Guardian Backup"
186 }
187 }
188 div class="col-lg-6" {
189 div class="alert alert-warning mb-0" {
190 strong { "Security Warning" }
191 br;
192 "Store this file securely since anyone with it and your password can run your guardian node."
193 }
194 }
195 }
196 }
197 }
198 }
199 }
200 };
201
202 Html(dashboard_layout(content, Some(&fedimintd_version)).into_string()).into_response()
203}
204
205pub fn router(api: DynDashboardApi) -> Router {
206 let mut app = Router::new()
207 .route(ROOT_ROUTE, get(dashboard_view))
208 .route(LOGIN_ROUTE, get(login_form).post(login_submit))
209 .route(EXPLORER_ROUTE, get(consensus_explorer_view))
210 .route(EXPLORER_IDX_ROUTE, get(consensus_explorer_view))
211 .route(DOWNLOAD_BACKUP_ROUTE, get(download_backup))
212 .with_static_routes();
213
214 if api
216 .get_module::<fedimint_lnv2_server::Lightning>()
217 .is_some()
218 {
219 app = app
220 .route(lnv2::LNV2_ADD_ROUTE, post(lnv2::post_add))
221 .route(lnv2::LNV2_REMOVE_ROUTE, post(lnv2::post_remove));
222 }
223
224 if api.get_module::<fedimint_meta_server::Meta>().is_some() {
226 app = app
227 .route(meta::META_SUBMIT_ROUTE, post(meta::post_submit))
228 .route(meta::META_SET_ROUTE, post(meta::post_set))
229 .route(meta::META_RESET_ROUTE, post(meta::post_reset))
230 .route(meta::META_DELETE_ROUTE, post(meta::post_delete));
231 }
232
233 app.with_state(UiState::new(api))
235}