1use 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
34const LOGO_URL: &str = "https://materialize.com/svgs/brand-guide/materialize-purple-mark.svg";
36
37fn 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#[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
85pub 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}