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
17pub 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}";
22
23pub fn common_head(title: &str) -> Markup {
24 html! {
25 meta charset="utf-8";
26 meta name="viewport" content="width=device-width, initial-scale=1.0";
27 title { "Guardian Dashboard"}
28 link rel="stylesheet" href="/assets/bootstrap.min.css" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous";
29 link rel="stylesheet" type="text/css" href="/assets/style.css";
30 link rel="icon" type="image/png" href="/assets/logo.png";
31
32 script defer src="/assets/htmx.org-2.0.4.min.js" {}
36
37 title { (title) }
38 }
39}
40
41#[derive(Debug, Deserialize)]
42pub(crate) struct LoginInput {
43 pub password: String,
44}
45
46#[derive(Clone)]
50pub struct UiState<T = DynDashboardApi> {
51 pub(crate) api: T,
52 pub(crate) auth_cookie_name: String,
53 pub(crate) auth_cookie_value: String,
54}
55
56impl<T> UiState<T> {
57 pub fn new(api: T) -> Self {
58 Self {
59 api,
60 auth_cookie_name: thread_rng().r#gen::<[u8; 4]>().encode_hex(),
61 auth_cookie_value: thread_rng().r#gen::<[u8; 32]>().encode_hex(),
62 }
63 }
64}
65
66pub(crate) fn login_layout(title: &str, content: Markup) -> Markup {
67 html! {
68 (DOCTYPE)
69 html {
70 head {
71 (common_head(title))
72 }
73 body {
74 div class="container" {
75 div class="row justify-content-center" {
76 div class="col-md-8 col-lg-5 narrow-container" {
77 header class="text-center" {
78 h1 class="header-title" { "Fedimint Guardian UI" }
79 }
80
81 div class="card" {
82 div class="card-body" {
83 (content)
84 }
85 }
86 }
87 }
88 }
89 script src="/assets/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous" {}
90 }
91 }
92 }
93}
94
95pub(crate) fn login_form_response() -> impl IntoResponse {
96 let content = html! {
97 form method="post" action="/login" {
98 div class="form-group mb-4" {
99 input type="password" class="form-control" id="password" name="password" placeholder="Your password" required;
100 }
101 div class="button-container" {
102 button type="submit" class="btn btn-primary setup-btn" { "Log In" }
103 }
104 }
105 };
106
107 Html(login_layout("Fedimint Guardian Login", content).into_string()).into_response()
108}
109
110pub(crate) fn login_submit_response(
111 auth: ApiAuth,
112 auth_cookie_name: String,
113 auth_cookie_value: String,
114 jar: CookieJar,
115 input: LoginInput,
116) -> impl IntoResponse {
117 if auth.0 == input.password {
118 let mut cookie = Cookie::new(auth_cookie_name, auth_cookie_value);
119
120 cookie.set_http_only(true);
121 cookie.set_same_site(Some(SameSite::Lax));
122
123 return (jar.add(cookie), Redirect::to("/")).into_response();
124 }
125
126 let content = html! {
127 div class="alert alert-danger" { "The password is invalid" }
128 div class="button-container" {
129 a href="/login" class="btn btn-primary setup-btn" { "Return to Login" }
130 }
131 };
132
133 Html(login_layout("Login Failed", content).into_string()).into_response()
134}