fedimint_server_ui/
lib.rs
1pub mod audit;
2pub mod dashboard;
3pub mod invite_code;
4pub mod latency;
5pub mod lnv2;
6pub mod setup;
7pub mod wallet;
8
9use axum::response::{Html, IntoResponse, Redirect};
10use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite};
11use fedimint_core::hex::ToHex;
12use fedimint_core::module::ApiAuth;
13use fedimint_core::secp256k1::rand::{Rng, thread_rng};
14use maud::{DOCTYPE, Markup, html};
15use serde::Deserialize;
16
17#[derive(Debug, Deserialize)]
18pub(crate) struct LoginInput {
19 pub password: String,
20}
21
22#[derive(Clone)]
24pub struct AuthState<T> {
25 pub(crate) api: T,
26 pub(crate) auth_cookie_name: String,
27 pub(crate) auth_cookie_value: String,
28}
29
30impl<T> AuthState<T> {
31 pub fn new(api: T) -> Self {
32 Self {
33 api,
34 auth_cookie_name: thread_rng().r#gen::<[u8; 4]>().encode_hex(),
35 auth_cookie_value: thread_rng().r#gen::<[u8; 32]>().encode_hex(),
36 }
37 }
38}
39
40pub fn common_styles() -> &'static str {
42 r#"
43 body {
44 background-color: #f8f9fa;
45 padding-top: 2rem;
46 padding-bottom: 2rem;
47 }
48
49 .header-title {
50 color: #0d6efd;
51 margin-bottom: 2rem;
52 }
53
54 .card {
55 border: none;
56 box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
57 border-radius: 0.5rem;
58 margin-bottom: 1rem;
59 }
60
61 .card-body {
62 padding: 1.25rem;
63 }
64
65 .card-header {
66 background-color: #fff;
67 border-bottom: 1px solid rgba(0, 0, 0, 0.125);
68 padding: 1rem 1.25rem;
69 }
70
71 /* Form elements */
72 .form-control {
73 padding: 0.75rem;
74 }
75
76 .form-group {
77 margin-bottom: 1rem;
78 }
79
80 .field-description {
81 font-size: 0.875rem;
82 color: #6c757d;
83 margin-top: 0.25rem;
84 }
85
86 .button-container {
87 margin-top: 2rem;
88 text-align: center;
89 }
90
91 /* Alert and status messages */
92 .error-message {
93 color: #dc3545;
94 margin-top: 1rem;
95 font-weight: 500;
96 }
97
98 .alert-info {
99 background-color: #e8f4f8;
100 border-color: #bee5eb;
101 }
102
103 /* Button styling */
104 .setup-btn {
105 width: auto;
106 min-width: 200px;
107 max-width: 300px;
108 padding: 0.5rem 1.5rem;
109 margin: 0 auto;
110 }
111
112 /* Responsive adjustments */
113 @media (min-width: 992px) {
114 .narrow-container {
115 max-width: 500px;
116 }
117 }
118
119 @media (max-width: 768px) {
120 .container {
121 padding-left: 15px;
122 padding-right: 15px;
123 }
124
125 .card-body {
126 padding: 1rem;
127 }
128 }
129 "#
130}
131
132pub(crate) fn login_layout(title: &str, content: Markup) -> Markup {
133 html! {
134 (DOCTYPE)
135 html {
136 head {
137 meta charset="utf-8";
138 meta name="viewport" content="width=device-width, initial-scale=1.0";
139 title { (title) }
140 link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous";
141 style {
142 (common_styles())
143 }
144 }
145 body {
146 div class="container" {
147 div class="row justify-content-center" {
148 div class="col-md-8 col-lg-5 narrow-container" {
149 header class="text-center" {
150 h1 class="header-title" { "Fedimint Guardian UI" }
151 }
152
153 div class="card" {
154 div class="card-body" {
155 (content)
156 }
157 }
158 }
159 }
160 }
161 script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous" {}
162 }
163 }
164 }
165}
166
167pub(crate) fn login_form_response() -> impl IntoResponse {
168 let content = html! {
169 form method="post" action="/login" {
170 div class="form-group mb-4" {
171 input type="password" class="form-control" id="password" name="password" placeholder="Your password" required;
172 }
173 div class="button-container" {
174 button type="submit" class="btn btn-primary setup-btn" { "Log In" }
175 }
176 }
177 };
178
179 Html(login_layout("Fedimint Guardian Login", content).into_string()).into_response()
180}
181
182pub(crate) fn login_submit_response(
183 auth: ApiAuth,
184 auth_cookie_name: String,
185 auth_cookie_value: String,
186 jar: CookieJar,
187 input: LoginInput,
188) -> impl IntoResponse {
189 if auth.0 == input.password {
190 let mut cookie = Cookie::new(auth_cookie_name, auth_cookie_value);
191
192 cookie.set_http_only(true);
193 cookie.set_same_site(Some(SameSite::Lax));
194
195 return (jar.add(cookie), Redirect::to("/")).into_response();
196 }
197
198 let content = html! {
199 div class="alert alert-danger" { "The password is invalid" }
200 div class="button-container" {
201 a href="/login" class="btn btn-primary setup-btn" { "Return to Login" }
202 }
203 };
204
205 Html(login_layout("Login Failed", content).into_string()).into_response()
206}
207
208pub(crate) async fn check_auth(
209 auth_cookie_name: &str,
210 auth_cookie_value: &str,
211 jar: &CookieJar,
212) -> bool {
213 match jar.get(auth_cookie_name) {
214 Some(cookie) => cookie.value() == auth_cookie_value,
215 None => false,
216 }
217}