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
18pub 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
67async 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
118async fn setup_submit(
120 State(state): State<UiState<DynSetupApi>>,
121 Form(input): Form<SetupInput>,
122) -> impl IntoResponse {
123 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
149async 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
158async 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
179async 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
257async 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
278async 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 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
315async 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}