fedimint_server_ui/
lib.rs

1pub(crate) mod assets;
2pub mod audit;
3pub mod dashboard;
4pub(crate) mod error;
5pub mod invite_code;
6pub mod latency;
7pub(crate) mod layout;
8pub mod lnv2;
9pub mod meta;
10pub mod setup;
11pub mod wallet;
12
13use axum::response::{Html, IntoResponse, Redirect};
14use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite};
15use fedimint_core::hex::ToHex;
16use fedimint_core::module::ApiAuth;
17use fedimint_core::secp256k1::rand::{Rng, thread_rng};
18use maud::{DOCTYPE, Markup, html};
19use serde::Deserialize;
20
21pub(crate) const LOG_UI: &str = "fm::ui";
22
23#[derive(Debug, Deserialize)]
24pub(crate) struct LoginInput {
25    pub password: String,
26}
27
28/// Generic state for both setup and dashboard UIs
29#[derive(Clone)]
30pub struct AuthState<T> {
31    pub(crate) api: T,
32    pub(crate) auth_cookie_name: String,
33    pub(crate) auth_cookie_value: String,
34}
35
36impl<T> AuthState<T> {
37    pub fn new(api: T) -> Self {
38        Self {
39            api,
40            auth_cookie_name: thread_rng().r#gen::<[u8; 4]>().encode_hex(),
41            auth_cookie_value: thread_rng().r#gen::<[u8; 32]>().encode_hex(),
42        }
43    }
44}
45
46pub(crate) fn login_layout(title: &str, content: Markup) -> Markup {
47    html! {
48        (DOCTYPE)
49        html {
50            head {
51                (layout::common_head(title))
52            }
53            body {
54                div class="container" {
55                    div class="row justify-content-center" {
56                        div class="col-md-8 col-lg-5 narrow-container" {
57                            header class="text-center" {
58                                h1 class="header-title" { "Fedimint Guardian UI" }
59                            }
60
61                            div class="card" {
62                                div class="card-body" {
63                                    (content)
64                                }
65                            }
66                        }
67                    }
68                }
69                script src="/assets/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous" {}
70            }
71        }
72    }
73}
74
75pub(crate) fn login_form_response() -> impl IntoResponse {
76    let content = html! {
77        form method="post" action="/login" {
78            div class="form-group mb-4" {
79                input type="password" class="form-control" id="password" name="password" placeholder="Your password" required;
80            }
81            div class="button-container" {
82                button type="submit" class="btn btn-primary setup-btn" { "Log In" }
83            }
84        }
85    };
86
87    Html(login_layout("Fedimint Guardian Login", content).into_string()).into_response()
88}
89
90pub(crate) fn login_submit_response(
91    auth: ApiAuth,
92    auth_cookie_name: String,
93    auth_cookie_value: String,
94    jar: CookieJar,
95    input: LoginInput,
96) -> impl IntoResponse {
97    if auth.0 == input.password {
98        let mut cookie = Cookie::new(auth_cookie_name, auth_cookie_value);
99
100        cookie.set_http_only(true);
101        cookie.set_same_site(Some(SameSite::Lax));
102
103        return (jar.add(cookie), Redirect::to("/")).into_response();
104    }
105
106    let content = html! {
107        div class="alert alert-danger" { "The password is invalid" }
108        div class="button-container" {
109            a href="/login" class="btn btn-primary setup-btn" { "Return to Login" }
110        }
111    };
112
113    Html(login_layout("Login Failed", content).into_string()).into_response()
114}
115
116pub(crate) async fn check_auth(
117    auth_cookie_name: &str,
118    auth_cookie_value: &str,
119    jar: &CookieJar,
120) -> bool {
121    match jar.get(auth_cookie_name) {
122        Some(cookie) => cookie.value() == auth_cookie_value,
123        None => false,
124    }
125}