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 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#[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 for p in addr.port()..65535 {
53 addr.set_port(p);
54 if let Ok(l) = tokio::net::TcpListener::bind(addr)
55 .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 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("/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)), )
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_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 axum::serve(
152 listener,
153 app.into_make_service_with_connect_info::<std::net::SocketAddr>(),
154 )
155 .into_future()
156 .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 .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 }
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 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.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 leptos_cfg
316 }
317
318 #[cfg(debug_assertions)]
319 fn get_basepath() -> String {
320 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 /><leptos_meta::MetaTags/>
366 </head>
367 <body>
368 <Main/>
369 </body>
370 </html>
371 }
372 })
373}