fedimint_server_ui/
setup.rs

1use axum::Router;
2use axum::extract::{Form, State};
3use axum::response::{Html, IntoResponse, Redirect};
4use axum::routing::{get, post};
5use axum_extra::extract::cookie::CookieJar;
6use fedimint_core::module::ApiAuth;
7use fedimint_server_core::setup_ui::DynSetupApi;
8use maud::{DOCTYPE, Markup, html};
9use serde::Deserialize;
10
11use crate::assets::WithStaticRoutesExt as _;
12use crate::auth::UserAuth;
13use crate::{
14    LOGIN_ROUTE, LoginInput, ROOT_ROUTE, UiState, common_head, login_form_response,
15    login_submit_response,
16};
17
18// Setup route constants
19pub const FEDERATION_SETUP_ROUTE: &str = "/federation_setup";
20pub const ADD_SETUP_CODE_ROUTE: &str = "/add_setup_code";
21pub const RESET_SETUP_CODES_ROUTE: &str = "/reset_setup_codes";
22pub const START_DKG_ROUTE: &str = "/start_dkg";
23
24#[derive(Debug, Deserialize)]
25pub(crate) struct SetupInput {
26    pub password: String,
27    pub name: String,
28    #[serde(default)]
29    pub is_lead: bool,
30    pub federation_name: String,
31}
32
33#[derive(Debug, Deserialize)]
34pub(crate) struct PeerInfoInput {
35    pub peer_info: String,
36}
37
38pub fn setup_layout(title: &str, content: Markup) -> Markup {
39    html! {
40        (DOCTYPE)
41        html {
42            head {
43                (common_head(title))
44            }
45            body {
46                div class="container" {
47                    div class="row justify-content-center" {
48                        div class="col-md-8 col-lg-5 narrow-container" {
49                            header class="text-center" {
50                                h1 class="header-title" { "Fedimint Guardian UI" }
51                            }
52
53                            div class="card" {
54                                div class="card-body" {
55                                    (content)
56                                }
57                            }
58                        }
59                    }
60                }
61                script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous" {}
62            }
63        }
64    }
65}
66
67// GET handler for the /setup route (display the setup form)
68async fn setup_form(State(state): State<UiState<DynSetupApi>>) -> impl IntoResponse {
69    if state.api.setup_code().await.is_some() {
70        return Redirect::to(FEDERATION_SETUP_ROUTE).into_response();
71    }
72
73    let content = html! {
74        form method="post" action=(ROOT_ROUTE) {
75            style {
76                r#"
77                .toggle-content {
78                    display: none;
79                }
80                
81                .toggle-control:checked ~ .toggle-content {
82                    display: block;
83                }
84                "#
85            }
86
87            div class="form-group mb-4" {
88                input type="text" class="form-control" id="name" name="name" placeholder="Guardian name" required;
89            }
90
91            div class="form-group mb-4" {
92                input type="password" class="form-control" id="password" name="password" placeholder="Secure password" required;
93            }
94
95            div class="form-group mb-4" {
96                div class="form-check" {
97                    input type="checkbox" class="form-check-input toggle-control" id="is_lead" name="is_lead" value="true";
98
99                    label class="form-check-label" for="is_lead" {
100                        "I am the guardian setting up the global configuration for this federation."
101                    }
102
103                    div class="toggle-content mt-3" {
104                        input type="text" class="form-control" id="federation_name" name="federation_name" placeholder="Federation name";
105                    }
106                }
107            }
108
109            div class="button-container" {
110                button type="submit" class="btn btn-primary setup-btn" { "Set Parameters" }
111            }
112        }
113    };
114
115    Html(setup_layout("Setup Fedimint Guardian", content).into_string()).into_response()
116}
117
118// POST handler for the /setup route (process the password setup form)
119async fn setup_submit(
120    State(state): State<UiState<DynSetupApi>>,
121    Form(input): Form<SetupInput>,
122) -> impl IntoResponse {
123    // Only use federation_name if is_lead is true
124    let federation_name = if input.is_lead {
125        Some(input.federation_name)
126    } else {
127        None
128    };
129
130    match state
131        .api
132        .set_local_parameters(ApiAuth(input.password), input.name, federation_name)
133        .await
134    {
135        Ok(_) => Redirect::to(LOGIN_ROUTE).into_response(),
136        Err(e) => {
137            let content = html! {
138                div class="alert alert-danger" { (e.to_string()) }
139                div class="button-container" {
140                    a href=(ROOT_ROUTE) class="btn btn-primary setup-btn" { "Return to Setup" }
141                }
142            };
143
144            Html(setup_layout("Setup Error", content).into_string()).into_response()
145        }
146    }
147}
148
149// GET handler for the /login route (display the login form)
150async fn login_form(State(state): State<UiState<DynSetupApi>>) -> impl IntoResponse {
151    if state.api.setup_code().await.is_none() {
152        return Redirect::to(ROOT_ROUTE).into_response();
153    }
154
155    login_form_response().into_response()
156}
157
158// POST handler for the /login route (authenticate and set session cookie)
159async fn login_submit(
160    State(state): State<UiState<DynSetupApi>>,
161    jar: CookieJar,
162    Form(input): Form<LoginInput>,
163) -> impl IntoResponse {
164    let auth = match state.api.auth().await {
165        Some(auth) => auth,
166        None => return Redirect::to(ROOT_ROUTE).into_response(),
167    };
168
169    login_submit_response(
170        auth,
171        state.auth_cookie_name,
172        state.auth_cookie_value,
173        jar,
174        input,
175    )
176    .into_response()
177}
178
179// GET handler for the /federation-setup route (main federation management page)
180async fn federation_setup(
181    State(state): State<UiState<DynSetupApi>>,
182    _auth: UserAuth,
183) -> impl IntoResponse {
184    let our_connection_info = state
185        .api
186        .setup_code()
187        .await
188        .expect("Successful authentication ensures that the local parameters have been set");
189
190    let connected_peers = state.api.connected_peers().await;
191
192    let content = html! {
193        section class="mb-4" {
194            div class="alert alert-info mb-3" {
195                (our_connection_info)
196            }
197
198            div class="text-center" {
199                button type="button" class="btn btn-outline-primary setup-btn"
200                    onclick=(format!("navigator.clipboard.writeText('{}')", our_connection_info)) {
201                    "Copy to Clipboard"
202                }
203            }
204        }
205
206        hr class="my-4" {}
207
208        section class="mb-4" {
209            ul class="list-group mb-4" {
210                @for peer in connected_peers {
211                    li class="list-group-item" { (peer) }
212                }
213            }
214
215            form method="post" action=(ADD_SETUP_CODE_ROUTE) {
216                div class="mb-3" {
217                    input type="text" class="form-control mb-2" id="peer_info" name="peer_info"
218                        placeholder="Paste setup code from fellow guardian" required;
219                }
220
221                div class="row mt-3" {
222                    div class="col-6" {
223                        button type="button" class="btn btn-warning w-100" onclick="document.getElementById('reset-form').submit();" {
224                            "Reset Guardians"
225                        }
226                    }
227
228                    div class="col-6" {
229                        button type="submit" class="btn btn-primary w-100" { "Add Guardian" }
230                    }
231                }
232            }
233
234            form id="reset-form" method="post" action=(RESET_SETUP_CODES_ROUTE) class="d-none" {}
235        }
236
237        hr class="my-4" {}
238
239        section class="mb-4" {
240            div class="alert alert-warning mb-4" {
241                "Make sure all information is correct and every guardian is ready before launching the federation. This process cannot be reversed once started."
242            }
243
244            div class="text-center" {
245                form method="post" action=(START_DKG_ROUTE) {
246                    button type="submit" class="btn btn-warning setup-btn" {
247                        "🚀 Launch Federation"
248                    }
249                }
250            }
251        }
252    };
253
254    Html(setup_layout("Federation Setup", content).into_string()).into_response()
255}
256
257// POST handler for adding peer connection info
258async fn post_add_setup_code(
259    State(state): State<UiState<DynSetupApi>>,
260    _auth: UserAuth,
261    Form(input): Form<PeerInfoInput>,
262) -> impl IntoResponse {
263    match state.api.add_peer_setup_code(input.peer_info).await {
264        Ok(..) => Redirect::to(FEDERATION_SETUP_ROUTE).into_response(),
265        Err(e) => {
266            let content = html! {
267                div class="alert alert-danger" { (e.to_string()) }
268                div class="button-container" {
269                    a href=(FEDERATION_SETUP_ROUTE) class="btn btn-primary setup-btn" { "Return to Setup" }
270                }
271            };
272
273            Html(setup_layout("Error", content).into_string()).into_response()
274        }
275    }
276}
277
278// POST handler for starting the DKG process
279async fn post_start_dkg(
280    State(state): State<UiState<DynSetupApi>>,
281    _auth: UserAuth,
282) -> impl IntoResponse {
283    match state.api.start_dkg().await {
284        Ok(()) => {
285            // Show simple DKG success page
286            let content = html! {
287                div class="alert alert-success my-4" {
288                    "The distributed key generation has been started successfully. You can monitor the progress in your server logs."
289                }
290                p class="text-center" {
291                    "Once the distributed key generation completes, the Guardian Dashboard will become available at the root URL."
292                }
293                div class="button-container mt-4" {
294                    a href=(ROOT_ROUTE) class="btn btn-primary setup-btn" {
295                        "Go to Dashboard"
296                    }
297                }
298            };
299
300            Html(setup_layout("DKG Started", content).into_string()).into_response()
301        }
302        Err(e) => {
303            let content = html! {
304                div class="alert alert-danger" { (e.to_string()) }
305                div class="button-container" {
306                    a href=(FEDERATION_SETUP_ROUTE) class="btn btn-primary setup-btn" { "Return to Setup" }
307                }
308            };
309
310            Html(setup_layout("Error", content).into_string()).into_response()
311        }
312    }
313}
314
315// POST handler for resetting peer connection info
316async fn post_reset_setup_codes(
317    State(state): State<UiState<DynSetupApi>>,
318    _auth: UserAuth,
319) -> impl IntoResponse {
320    state.api.reset_setup_codes().await;
321
322    Redirect::to(FEDERATION_SETUP_ROUTE).into_response()
323}
324
325pub fn router(api: DynSetupApi) -> Router {
326    Router::new()
327        .route(ROOT_ROUTE, get(setup_form).post(setup_submit))
328        .route(LOGIN_ROUTE, get(login_form).post(login_submit))
329        .route(FEDERATION_SETUP_ROUTE, get(federation_setup))
330        .route(ADD_SETUP_CODE_ROUTE, post(post_add_setup_code))
331        .route(RESET_SETUP_CODES_ROUTE, post(post_reset_setup_codes))
332        .route(START_DKG_ROUTE, post(post_start_dkg))
333        .with_static_routes()
334        .with_state(UiState::new(api))
335}