fedimint_server_ui/
lib.rs

1pub mod assets;
2pub(crate) mod auth;
3pub mod dashboard;
4pub mod setup;
5
6use axum::response::{Html, IntoResponse, Redirect};
7use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite};
8use fedimint_core::hex::ToHex;
9use fedimint_core::module::ApiAuth;
10use fedimint_core::secp256k1::rand::{Rng, thread_rng};
11use fedimint_server_core::dashboard_ui::DynDashboardApi;
12use maud::{DOCTYPE, Markup, html};
13use serde::Deserialize;
14
15pub(crate) const LOG_UI: &str = "fm::ui";
16
17// Common route constants
18pub const ROOT_ROUTE: &str = "/";
19pub const LOGIN_ROUTE: &str = "/login";
20pub const EXPLORER_IDX_ROUTE: &str = "/explorer";
21pub const EXPLORER_ROUTE: &str = "/explorer/{session_idx}";
22pub const DOWNLOAD_BACKUP_ROUTE: &str = "/download-backup";
23
24pub fn common_head(title: &str) -> Markup {
25    html! {
26        meta charset="utf-8";
27        meta name="viewport" content="width=device-width, initial-scale=1.0";
28        title { "Guardian Dashboard"}
29        link rel="stylesheet" href="/assets/bootstrap.min.css" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous";
30        link rel="stylesheet" type="text/css" href="/assets/style.css";
31        link rel="icon" type="image/png" href="/assets/logo.png";
32
33        // Note: this needs to be included in the header, so that web-page does not
34        // get in a state where htmx is not yet loaded. `deref` helps with blocking the load.
35        // Learned the hard way. --dpc
36        script defer src="/assets/htmx.org-2.0.4.min.js" {}
37
38        title { (title) }
39    }
40}
41
42#[derive(Debug, Deserialize)]
43pub(crate) struct LoginInput {
44    pub password: String,
45}
46
47/// Generic state for both setup and dashboard UIs
48///
49/// Most of code is written for dashboard, so `T` defaults to `DynDashboardApi`
50#[derive(Clone)]
51pub struct UiState<T = DynDashboardApi> {
52    pub(crate) api: T,
53    pub(crate) auth_cookie_name: String,
54    pub(crate) auth_cookie_value: String,
55}
56
57impl<T> UiState<T> {
58    pub fn new(api: T) -> Self {
59        Self {
60            api,
61            auth_cookie_name: thread_rng().r#gen::<[u8; 4]>().encode_hex(),
62            auth_cookie_value: thread_rng().r#gen::<[u8; 32]>().encode_hex(),
63        }
64    }
65}
66
67pub(crate) fn login_layout(title: &str, content: Markup) -> Markup {
68    html! {
69        (DOCTYPE)
70        html {
71            head {
72                (common_head(title))
73            }
74            body {
75                div class="container" {
76                    div class="row justify-content-center" {
77                        div class="col-md-8 col-lg-5 narrow-container" {
78                            header class="text-center" {
79                                h1 class="header-title" { "Fedimint Guardian UI" }
80                            }
81
82                            div class="card" {
83                                div class="card-body" {
84                                    (content)
85                                }
86                            }
87                        }
88                    }
89                }
90                script src="/assets/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous" {}
91            }
92        }
93    }
94}
95
96pub(crate) fn login_form_response() -> impl IntoResponse {
97    let content = html! {
98        form method="post" action="/login" {
99            div class="form-group mb-4" {
100                input type="password" class="form-control" id="password" name="password" placeholder="Your password" required;
101            }
102            div class="button-container" {
103                button type="submit" class="btn btn-primary setup-btn" { "Log In" }
104            }
105        }
106    };
107
108    Html(login_layout("Fedimint Guardian Login", content).into_string()).into_response()
109}
110
111pub(crate) fn login_submit_response(
112    auth: ApiAuth,
113    auth_cookie_name: String,
114    auth_cookie_value: String,
115    jar: CookieJar,
116    input: LoginInput,
117) -> impl IntoResponse {
118    if auth.0 == input.password {
119        let mut cookie = Cookie::new(auth_cookie_name, auth_cookie_value);
120
121        cookie.set_http_only(true);
122        cookie.set_same_site(Some(SameSite::Lax));
123
124        return (jar.add(cookie), Redirect::to("/")).into_response();
125    }
126
127    let content = html! {
128        div class="alert alert-danger" { "The password is invalid" }
129        div class="button-container" {
130            a href="/login" class="btn btn-primary setup-btn" { "Return to Login" }
131        }
132    };
133
134    Html(login_layout("Login Failed", content).into_string()).into_response()
135}