Skip to main content

flams_main/server/
mod.rs

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