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.clone();
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.clone())
55 .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 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("/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)), )
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_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 axum::serve(
150 listener,
151 app.into_make_service_with_connect_info::<std::net::SocketAddr>(),
152 )
153 .into_future()
154 .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 .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 }
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 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.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 leptos_cfg
314 }
315
316 #[cfg(debug_assertions)]
317 fn get_basepath() -> String {
318 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 /><leptos_meta::MetaTags/>
365 </head>
366 <body>
367 <Main/>
368 </body>
369 </html>
370 </SSRMountStyleProvider>
371 }
372}