mz/
server.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10use std::future::{Future, IntoFuture};
11use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
12
13use axum::{
14    Router,
15    extract::Query,
16    response::{Html, IntoResponse, Response},
17    routing::get,
18};
19use mz_frontegg_auth::AppPassword;
20use serde::Deserialize;
21use tokio::net::TcpListener;
22use tokio::sync::mpsc::UnboundedSender;
23use uuid::Uuid;
24
25use crate::error::Error;
26
27#[derive(Debug, Deserialize)]
28#[serde(rename_all = "camelCase")]
29struct BrowserAPIToken {
30    client_id: String,
31    secret: String,
32}
33
34// Please update this link if the logo location changes in the future.
35const LOGO_URL: &str = "https://materialize.com/svgs/brand-guide/materialize-purple-mark.svg";
36
37/// Produces an HTML string formatted
38/// with a message centered in the middle of the page
39/// and Materialize logo on top
40fn format_as_html_message(msg: &str) -> Html<String> {
41    Html(String::from(&format!(" \
42        <body style=\"margin: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; background-color: #f0f0f0;\">
43            <div style=\"text-align: center; padding: 100px; background-color: #ffffff; border-radius: 10px; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);\"> \
44                <img src=\"{}\"> \
45                <h2 style=\"padding-top: 20px; font-family: Inter, Arial, sans-serif;\">{}</h2> \
46            </div>
47        </body>
48    ", LOGO_URL, msg)))
49}
50
51/// Request handler for the server waiting the browser API token creation
52/// Axum requires the handler be async even though we don't await
53#[allow(clippy::unused_async)]
54async fn request(
55    Query(BrowserAPIToken { secret, client_id }): Query<BrowserAPIToken>,
56    tx: UnboundedSender<Result<AppPassword, Error>>,
57) -> Response {
58    if secret.len() == 0 && client_id.len() == 0 {
59        tx.send(Err(Error::LoginOperationCanceled))
60            .unwrap_or_else(|_| panic!("Error handling login details."));
61        return format_as_html_message("Login canceled. You can now close the tab.")
62            .into_response();
63    }
64
65    let client_id = client_id.parse::<Uuid>();
66    let secret = secret.parse::<Uuid>();
67    if let (Ok(client_id), Ok(secret)) = (client_id, secret) {
68        let app_password = AppPassword {
69            client_id,
70            secret_key: secret,
71        };
72        tx.send(Ok(app_password))
73            .unwrap_or_else(|_| panic!("Error handling login details."));
74        format_as_html_message("You can now close the tab.").into_response()
75    } else {
76        tx.send(Err(Error::InvalidAppPassword))
77            .unwrap_or_else(|_| panic!("Error handling login details."));
78        format_as_html_message(
79            "Invalid credentials. Please, try again or communicate with support.",
80        )
81        .into_response()
82    }
83}
84
85/// Server for handling login's information.
86pub async fn server(
87    tx: UnboundedSender<Result<AppPassword, Error>>,
88) -> (impl Future<Output = Result<(), std::io::Error>>, u16) {
89    let app = Router::new().route("/", get(|body| request(body, tx)));
90    let addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0));
91    let listener = TcpListener::bind(addr).await.unwrap_or_else(|e| {
92        panic!("error binding to {}: {}", addr, e);
93    });
94    let port = listener.local_addr().unwrap().port();
95    let server = axum::serve(listener, app.into_make_service());
96
97    (server.into_future(), port)
98}