fedimint_gateway_ui/
lib.rs

1mod 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
49// Dashboard login submit handler
50async 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}