Skip to main content

flams/server/
mod.rs

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