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