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