flams/server/
mod.rs

1pub mod files;
2pub mod lsp;
3pub mod settings;
4
5use std::future::IntoFuture;
6
7use axum::{
8    error_handling::HandleErrorLayer,
9    extract,
10    response::{IntoResponse, Redirect},
11    Router,
12};
13use axum_login::AuthManagerLayerBuilder;
14use axum_macros::FromRef;
15use flams_database::DBBackend;
16use flams_git::gl::auth::GitLabOAuth;
17use flams_system::settings::Settings;
18use http::{StatusCode, Uri};
19use leptos::prelude::*;
20use leptos_axum::{generate_route_list, LeptosRoutes};
21use tower::ServiceBuilder;
22use tower_sessions::{Expiry, MemoryStore};
23use tracing::{instrument, Instrument};
24
25use flams_router_dashboard::{
26    ws::{self, WebSocketServer},
27    Main,
28};
29
30lazy_static::lazy_static! {
31    static ref SERVER_SPAN:tracing::Span = {
32        //println!("Here!");
33        tracing::info_span!(target:"server",parent:None,"server")
34    };
35}
36
37#[inline]
38pub async fn run(port_channel: Option<tokio::sync::watch::Sender<Option<u16>>>) {
39    run_i(port_channel).instrument(SERVER_SPAN.clone()).await
40}
41
42/// ### Panics
43#[instrument(level = "info", target = "server", name = "run", skip_all)]
44async fn run_i(port_channel: Option<tokio::sync::watch::Sender<Option<u16>>>) {
45    let mut state = ServerState::new().in_current_span().await;
46    let mut addr = state.options.site_addr.clone();
47    let mut changed = false;
48    let mut listener = None;
49    //let span = tracing::info_span!(target:"server","request");
50    for p in addr.port()..65535 {
51        addr.set_port(p);
52        if let Ok(l) = tokio::net::TcpListener::bind(addr.clone())
53            //.instrument(span.clone())
54            .await
55        {
56            listener = Some(l);
57            break;
58        } else {
59            changed = true;
60        }
61    }
62    let listener = listener.expect("Could not bind to any port");
63
64    // avoid error: reactive_graph-0.1.7/src/owner/arena.rs:60:29, the `sandboxed-arenas` feature is active, but no Arena is active
65    leptos::prelude::Owner::new().set();
66
67    if changed {
68        if port_channel.is_some() {
69            tracing::warn!("Port already in use; used {} instead", addr.port());
70        } else {
71            println!("Port already in use; used {} instead", addr.port());
72        }
73        flams_system::settings::Settings::get()
74            .port
75            .store(addr.port(), std::sync::atomic::Ordering::Relaxed);
76        state.options.site_addr = addr;
77    }
78
79    let session_store = MemoryStore::default();
80    let session_layer = tower_sessions::SessionManagerLayer::new(session_store)
81        .with_expiry(Expiry::OnInactivity(
82            tower_sessions::cookie::time::Duration::days(5),
83        ))
84        .with_secure(false)
85        .with_same_site(tower_sessions::cookie::SameSite::Lax);
86
87    let auth_layer = ServiceBuilder::new()
88        .layer(HandleErrorLayer::new(|_| async {
89            http::StatusCode::BAD_REQUEST
90        }))
91        .layer(AuthManagerLayerBuilder::new(state.db.clone(), session_layer).build());
92
93    let routes = generate_route_list(Main);
94
95    let has_gl = state.oauth.is_some();
96
97    let mut app = axum::Router::<ServerState>::new()
98        .route("/ws/log", axum::routing::get(ws::LogSocket::ws_handler))
99        .route("/ws/queue", axum::routing::get(ws::QueueSocket::ws_handler))
100        .route("/ws/lsp", axum::routing::get(crate::server::lsp::register));
101
102    if has_gl {
103        app = app //.route("/gl_login", axum::routing::get(gl::gl_login))
104            .route("/gitlab_login", axum::routing::get(gl_cont));
105    }
106
107    let app = app
108        .route(
109            "/api/{*fn_name}",
110            axum::routing::get(server_fn_handle).post(server_fn_handle),
111        )
112        .route(
113            "/content/{*fn_name}",
114            axum::routing::get(server_fn_handle).post(server_fn_handle),
115        )
116        .leptos_routes_with_handler(
117            routes,
118            axum::routing::get(|a, b, c| routes_handler(a, b, c)), //.in_current_span()),
119        )
120        .route("/img", axum::routing::get(files::img_handler))
121        .route("/doc", axum::routing::get(files::doc_handler))
122        .fallback(file_and_error_handler)
123        .layer(auth_layer)
124        .layer(
125            tower_http::cors::CorsLayer::new()
126                .allow_methods([http::Method::GET, http::Method::POST])
127                .allow_origin(tower_http::cors::Any)
128                //.allow_credentials(true)
129                .allow_headers([http::header::COOKIE, http::header::SET_COOKIE]),
130        )
131        .layer(tower_http::trace::TraceLayer::new_for_http().make_span_with(SpanLayer));
132    let app: Router<()> = app.with_state(state);
133
134    if let Some(channel) = port_channel {
135        channel
136            .send(Some(addr.port()))
137            .expect("Error sending port address");
138    }
139
140    crate::fns::init();
141
142    axum::serve(
143        listener,
144        app.into_make_service_with_connect_info::<std::net::SocketAddr>(),
145    )
146    .into_future()
147    //.instrument(span)
148    //.in_current_span()
149    .await
150    .unwrap_or_else(|e| panic!("{e}"));
151}
152
153async fn gl_cont(
154    extract::Query(params): extract::Query<flams_git::gl::auth::AuthRequest>,
155    extract::State(state): extract::State<ServerState>,
156    mut auth_session: axum_login::AuthSession<DBBackend>,
157) -> Result<axum::response::Response, StatusCode> {
158    let oauth = state.oauth.as_ref().unwrap_or_else(|| unreachable!());
159    let token = oauth
160        .callback(params)
161        .await
162        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
163    let gl = flams_git::gl::GLInstance::global()
164        .get()
165        .await
166        .unwrap_or_else(|| unreachable!());
167    let user = gl
168        .get_oauth_user(&token)
169        .await
170        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
171    if let Ok(Some(u)) = state.db.add_user(user, token.secret().clone()).await {
172        let _ = auth_session.login(&u).await;
173    }
174    Ok(Redirect::to("/dashboard").into_response())
175}
176
177async fn routes_handler(
178    auth_session: axum_login::AuthSession<DBBackend>,
179    extract::State(ServerState {
180        db, options, oauth, ..
181    }): extract::State<ServerState>,
182    request: http::Request<axum::body::Body>,
183) -> Result<impl IntoResponse, StatusCode> {
184    use futures::future::FutureExt;
185    let handler = leptos_axum::render_app_to_stream_with_context(
186        move || {
187            provide_context(auth_session.clone());
188            provide_context(db.clone());
189            provide_context(oauth.clone());
190        },
191        move || shell(options.clone()),
192    );
193    std::panic::AssertUnwindSafe(handler(request))
194        .catch_unwind()
195        .await
196        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
197}
198
199async fn server_fn_handle(
200    auth_session: axum_login::AuthSession<DBBackend>,
201    extract::State(ServerState {
202        db, options, oauth, ..
203    }): extract::State<ServerState>,
204    request: http::Request<axum::body::Body>,
205) -> impl IntoResponse {
206    leptos_axum::handle_server_fns_with_context(
207        move || {
208            provide_context(auth_session.clone());
209            provide_context(options.clone());
210            provide_context(db.clone());
211            provide_context(oauth.clone());
212        },
213        request,
214    )
215    //.in_current_span()
216    .await
217}
218
219async fn file_and_error_handler(
220    mut uri: Uri,
221    extract::State(state): extract::State<ServerState>,
222    request: http::Request<axum::body::Body>,
223) -> axum::response::Response {
224    let r = leptos_axum::file_and_error_handler(shell);
225    if uri.path().ends_with("flams_bg.wasm") {
226        // change to "flams.wasm"
227        uri = Uri::builder()
228            .path_and_query("/pkg/flams.wasm")
229            .build()
230            .unwrap_or_else(|_| unreachable!());
231    }
232    r(uri, extract::State(state), request)
233        //.in_current_span()
234        .await
235}
236
237#[derive(Clone)]
238struct SpanLayer;
239impl<A> tower_http::trace::MakeSpan<A> for SpanLayer {
240    fn make_span(&mut self, r: &http::Request<A>) -> tracing::Span {
241        //println!("Here: {},{}",r.method(),r.uri());
242        tracing::span!(
243            parent:&*SERVER_SPAN,
244            tracing::Level::INFO,
245            "request",
246            method = %r.method(),
247            uri = %r.uri(),
248            version = ?r.version(),
249        )
250    }
251}
252
253#[derive(Clone, FromRef)]
254pub(crate) struct ServerState {
255    options: LeptosOptions,
256    db: DBBackend,
257    pub(crate) images: files::ImageStore,
258    pub(crate) oauth: Option<GitLabOAuth>,
259}
260
261impl ServerState {
262    async fn new() -> Self {
263        let leptos_cfg = Self::setup_leptos();
264        let redirect = Settings::get().gitlab_redirect_url.as_ref();
265        let oauth = if let Some(redirect) = redirect {
266            flams_git::gl::GLInstance::global()
267                .get()
268                .await
269                .and_then(|gl| gl.new_oauth(&format!("{redirect}/gitlab_login")).ok())
270        } else {
271            None
272        };
273        let db = DBBackend::new().in_current_span().await;
274        Self {
275            options: leptos_cfg.leptos_options,
276            db,
277            images: files::ImageStore::default(),
278            oauth,
279        }
280    }
281
282    fn setup_leptos() -> ConfFile {
283        let basepath = Self::get_basepath();
284        let mut leptos_cfg =
285            leptos::prelude::get_configuration(None).expect("Failed to get leptos config");
286        leptos_cfg.leptos_options.site_root = basepath.into();
287        leptos_cfg.leptos_options.output_name = "flams".into();
288
289        let settings = Settings::get();
290        let ip = settings.ip;
291        let port = settings.port();
292        leptos_cfg.leptos_options.site_addr = std::net::SocketAddr::new(ip, port);
293        leptos_cfg
294    }
295
296    #[cfg(debug_assertions)]
297    fn get_basepath() -> String {
298        if std::env::var("LEPTOS_OUTPUT_NAME").is_err() {
299            unsafe { std::env::set_var("LEPTOS_OUTPUT_NAME", "flams") };
300        }
301        if Settings::get().lsp {
302            let Ok(p) = std::env::current_exe()
303                .expect("Error setting current web-dir path")
304                .parent()
305                .expect("Error setting current web-dir path")
306                .parent()
307                .expect("Error setting current web-dir path")
308                .join("web")
309                .canonicalize()
310            else {
311                panic!("Failed to canonicalize path");
312            };
313            p.display().to_string()
314        } else {
315            "target/web".to_string()
316        }
317    }
318    #[cfg(not(debug_assertions))]
319    fn get_basepath() -> String {
320        std::env::current_exe()
321            .ok()
322            .and_then(|p| p.parent().map(|p| p.join("web")))
323            .expect("Failed to determine executable path")
324            .display()
325            .to_string()
326    }
327}
328
329fn shell(options: LeptosOptions) -> impl IntoView {
330    //use thaw::ssr::SSRMountStyleProvider;
331    view! {
332    //    <SSRMountStyleProvider>
333            <!DOCTYPE html>
334            <html lang="en">
335                <head>
336                    <meta charset="utf-8"/>
337                    <meta name="viewport" content="width=device-width, initial-scale=1"/>
338                    {
339                        #[cfg(debug_assertions)]
340                        {view!(<AutoReload options=options.clone() />)}
341                    }
342                    <HydrationScripts options />//islands=true/>
343                    <leptos_meta::MetaTags/>
344                </head>
345                <body>
346                    <Main/>
347                </body>
348            </html>
349    //    </SSRMountStyleProvider>
350    }
351}