fedimint_gateway_ui/
lib.rs1mod federation;
2mod general;
3mod lightning;
4
5use std::fmt::Display;
6use std::sync::Arc;
7
8use async_trait::async_trait;
9use axum::extract::State;
10use axum::response::{Html, IntoResponse, Redirect};
11use axum::routing::get;
12use axum::{Form, Router};
13use axum_extra::extract::CookieJar;
14use axum_extra::extract::cookie::{Cookie, SameSite};
15use fedimint_gateway_common::GatewayInfo;
16use fedimint_ui_common::assets::WithStaticRoutesExt;
17use fedimint_ui_common::auth::UserAuth;
18use fedimint_ui_common::{
19 LOGIN_ROUTE, LoginInput, ROOT_ROUTE, UiState, dashboard_layout, login_form_response,
20 login_layout,
21};
22use maud::html;
23
24use crate::lightning::channels_fragment_handler;
25
26pub type DynGatewayApi<E> = Arc<dyn IAdminGateway<Error = E> + Send + Sync + 'static>;
27
28pub(crate) const CHANNEL_FRAGMENT_ROUTE: &str = "/channels/fragment";
29
30#[async_trait]
31pub trait IAdminGateway {
32 type Error;
33
34 async fn handle_get_info(&self) -> Result<GatewayInfo, Self::Error>;
35
36 async fn handle_list_channels_msg(
37 &self,
38 ) -> Result<Vec<fedimint_gateway_common::ChannelInfo>, Self::Error>;
39
40 fn get_password_hash(&self) -> String;
41
42 fn gatewayd_version(&self) -> String;
43}
44
45async fn login_form<E>(State(_state): State<UiState<DynGatewayApi<E>>>) -> impl IntoResponse {
46 login_form_response("Fedimint Gateway Login")
47}
48
49async fn login_submit<E>(
51 State(state): State<UiState<DynGatewayApi<E>>>,
52 jar: CookieJar,
53 Form(input): Form<LoginInput>,
54) -> impl IntoResponse {
55 if let Ok(verify) = bcrypt::verify(input.password, &state.api.get_password_hash())
56 && verify
57 {
58 let mut cookie = Cookie::new(state.auth_cookie_name.clone(), state.auth_cookie_value);
59 cookie.set_path(ROOT_ROUTE);
60
61 cookie.set_http_only(true);
62 cookie.set_same_site(Some(SameSite::Lax));
63
64 let jar = jar.add(cookie);
65 return (jar, Redirect::to(ROOT_ROUTE)).into_response();
66 }
67
68 let content = html! {
69 div class="alert alert-danger" { "The password is invalid" }
70 div class="button-container" {
71 a href=(LOGIN_ROUTE) class="btn btn-primary setup-btn" { "Return to Login" }
72 }
73 };
74
75 Html(login_layout("Login Failed", content).into_string()).into_response()
76}
77
78async fn dashboard_view<E>(
79 State(state): State<UiState<DynGatewayApi<E>>>,
80 _auth: UserAuth,
81) -> impl IntoResponse
82where
83 E: std::fmt::Display,
84{
85 let gatewayd_version = state.api.gatewayd_version();
86 let gateway_info = match state.api.handle_get_info().await {
87 Ok(info) => info,
88 Err(err) => {
89 let content = html! {
90 div class="alert alert-danger mt-4" {
91 strong { "Failed to fetch gateway info: " }
92 (err.to_string())
93 }
94 };
95 return Html(
96 dashboard_layout(content, "Fedimint Gateway UI", Some(&gatewayd_version))
97 .into_string(),
98 )
99 .into_response();
100 }
101 };
102
103 let content = html! {
104 div class="row gy-4" {
105 div class="col-md-12" {
106 (general::render(&gateway_info))
107 }
108 }
109
110 div class="row gy-4 mt-2" {
111 div class="col-md-12" {
112 (lightning::render(&gateway_info, &state.api).await)
113 }
114 }
115
116 @for fed in gateway_info.federations {
117 (federation::render(&fed))
118 }
119 };
120
121 Html(dashboard_layout(content, "Fedimint Gateway UI", Some(&gatewayd_version)).into_string())
122 .into_response()
123}
124
125pub fn router<E: Display + 'static>(api: DynGatewayApi<E>) -> Router {
126 let app = Router::new()
127 .route(ROOT_ROUTE, get(dashboard_view))
128 .route(LOGIN_ROUTE, get(login_form).post(login_submit))
129 .route(CHANNEL_FRAGMENT_ROUTE, get(channels_fragment_handler))
130 .with_static_routes();
131
132 app.with_state(UiState::new(api))
133}