1#![allow(clippy::must_use_candidate)]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3
4#[cfg(any(
5 all(feature = "ssr", feature = "hydrate", not(feature = "docs-only")),
6 not(any(feature = "ssr", feature = "hydrate"))
7))]
8compile_error!("exactly one of the features \"ssr\" or \"hydrate\" must be enabled");
9
10mod login_state;
11use ftml_uris::FtmlUri;
12pub use login_state::*;
13#[cfg(feature = "ssr")]
14pub mod uris;
15pub mod ws;
16
17#[macro_export]
18macro_rules! maybe_lazy {
19 ($t:path) => {{
20 #[cfg(any(debug_assertions,feature="docs-only"))]
21 {$t}
22 #[cfg(not(any(debug_assertions,feature="docs-only")))]
23 {leptos_router::Lazy::<$t>::new()}
24 }};
25 ($name:ident = $e:expr) => {
26 #[cfg(any(debug_assertions,feature="docs-only"))]
27 #[component]
28 pub fn $name() -> AnyView { $e }
29 #[cfg(not(any(debug_assertions,feature="docs-only")))]
30 #[derive(Debug,Clone,serde::Deserialize)]
31 pub struct $name;
32 #[cfg(not(any(debug_assertions,feature="docs-only")))]
33 #[leptos_router::lazy_route]
34 impl leptos_router::LazyRoute for $name {
35 fn data() -> Self {
36 Self
37 }
38 fn view($name:Self) -> leptos::prelude::AnyView {
39 $e}
41 }
42 }
43}
44
45use leptos::{either::EitherOf3, prelude::*};
46
47pub fn vscode_link(archive: &ftml_uris::ArchiveId, rel_path: &str) -> impl IntoView + use<> {
48 let href = format!("vscode://kwarc.flams/open?a={archive}&rp={rel_path}");
49 view! {
50 <a href=href><ftml_component_utils::icons::VSCodeIcon/></a>
51 }
52}
53
54#[component]
55pub fn RequireLogin(children: Children) -> impl IntoView {
56 require_login(children)
57}
58
59pub fn require_login(children: Children) -> AnyView {
60 use flams_web_utils::components::display_error;
61 use ftml_component_utils::Spinner;
62
63 let children = std::sync::Arc::new(flams_utils::parking_lot::Mutex::new(Some(children)));
64 (move || match LoginState::get() {
65 LoginState::Loading => view!(<Spinner/>).into_any(),
66 LoginState::Admin | LoginState::NoAccounts | LoginState::User { is_admin: true, .. } => {
67 (children.clone().lock().take()).map(|f| f()).into_any()
68 }
69 _ => view!(<div>{display_error("Not logged in".into())}</div>).into_any(),
70 })
71 .into_any()
72}
73
74#[cfg(feature = "ssr")]
75pub fn get_oauth() -> Result<(flams_git::gl::auth::GitLabOAuth, String), String> {
77 use flams_git::gl::auth::GitLabOAuth;
78 use leptos::prelude::*;
79 let Some(session) = use_context::<axum_login::AuthSession<flams_database::DBBackend>>() else {
80 return Err("Internal Error".to_string());
81 };
82 let Some(user) = session.user else {
83 return Err("Not logged in".to_string());
84 };
85 let Some(oauth): Option<GitLabOAuth> = expect_context() else {
86 return Err("Not Gitlab integration set up".to_string());
87 };
88 Ok((oauth, user.secret))
89}
90
91pub trait ServerFnExt {
92 type Output;
93 type Error;
94 #[cfg(feature = "hydrate")]
95 #[allow(async_fn_in_trait)]
96 async fn call_remote(self, url: String) -> Result<Self::Output, Self::Error>;
97}
98
99#[cfg(feature = "hydrate")]
100mod hydrate {
101 use super::ServerFnExt;
102 use bytes::Bytes;
103 use futures::{Stream, StreamExt};
104 use leptos::server_fn::codec::{Encoding, FromReq, FromRes, IntoReq, IntoRes};
105 use leptos::server_fn::request::BrowserMockReq;
106 use leptos::server_fn::request::browser::{BrowserRequest as OrigBrowserRequest, Request};
107 use leptos::server_fn::response::BrowserMockRes;
108 use leptos::server_fn::response::browser::BrowserResponse as OrigBrowserResponse;
109 use leptos::{
110 prelude::*,
111 server_fn::{request::ClientReq, response::ClientRes},
112 wasm_bindgen::JsCast,
113 };
114 use send_wrapper::SendWrapper;
115 use wasm_streams::ReadableStream;
116
117 struct Paired<
120 In,
121 F: leptos::server_fn::ServerFn<
122 Client = leptos::server_fn::client::browser::BrowserClient,
123 Server = leptos::server_fn::mock::BrowserMockServer,
124 Protocol = leptos::server_fn::Http<In, server_fn::codec::Json>, >,
126 > {
127 sfn: F,
128 url: String,
129 }
130
131 struct BrowserRequest(SendWrapper<Request>);
132 struct BrowserFormData(SendWrapper<leptos::web_sys::FormData>);
133 struct EncodingWrap<E: Encoding>(E);
134
135 impl<E: Encoding> server_fn::ContentType for EncodingWrap<E> {
136 const CONTENT_TYPE: &'static str = E::CONTENT_TYPE;
137 }
138 impl<E: Encoding> Encoding for EncodingWrap<E> {
139 const METHOD: http::Method = E::METHOD;
140 }
141
142 impl<
143 In: Encoding,
144 Err: Send + Sync + FromServerFnError,
145 F: leptos::server_fn::ServerFn<
146 Client = leptos::server_fn::client::browser::BrowserClient,
147 Server = leptos::server_fn::mock::BrowserMockServer,
148 Protocol = leptos::server_fn::Http<In, server_fn::codec::Json>,
149 Error = Err,
150 InputStreamError = Err,
151 OutputStreamError = Err,
152 > + FromReq<In, BrowserMockReq, Err>,
153 > FromReq<EncodingWrap<In>, BrowserMockReq, Err> for Paired<In, F>
154 {
155 async fn from_req(req: BrowserMockReq) -> Result<Self, Err> {
156 Ok(Self {
157 sfn: <F as FromReq<In, BrowserMockReq, Err>>::from_req(req).await?,
158 url: String::new(),
159 })
160 }
161 }
162
163 impl<
164 In: Encoding,
165 Err: Send + Sync + FromServerFnError,
166 F: leptos::server_fn::ServerFn<
167 Client = leptos::server_fn::client::browser::BrowserClient,
168 Server = leptos::server_fn::mock::BrowserMockServer,
169 Protocol = leptos::server_fn::Http<In, server_fn::codec::Json>,
170 Error = Err,
171 InputStreamError = Err,
172 OutputStreamError = Err,
173 > + IntoReq<In, OrigBrowserRequest, Err>,
174 > IntoReq<EncodingWrap<In>, BrowserRequest, Err> for Paired<In, F>
175 {
176 fn into_req(self, _path: &str, accepts: &str) -> Result<BrowserRequest, Err> {
177 let Paired { sfn, url } = self;
178 let path = format!("{}{}", url, F::PATH);
179 let req = <F as IntoReq<In, OrigBrowserRequest, _>>::into_req(sfn, &path, accepts)?;
180 let req: Request = req.into();
181 Ok(BrowserRequest(SendWrapper::new(req)))
182 }
183 }
184
185 impl<
186 In: Encoding,
187 Err: Send + Sync + FromServerFnError,
188 F: leptos::server_fn::ServerFn<
189 Client = leptos::server_fn::client::browser::BrowserClient,
190 Server = leptos::server_fn::mock::BrowserMockServer,
191 Protocol = leptos::server_fn::Http<In, server_fn::codec::Json>,
192 Error = Err,
193 InputStreamError = Err,
194 OutputStreamError = Err,
195 >,
196 > leptos::server_fn::ServerFn for Paired<In, F>
197 where
198 F: FromReq<In, BrowserMockReq, Err>
199 + IntoReq<In, OrigBrowserRequest, Err>
200 + server_fn::serde::Serialize
201 + server_fn::serde::de::DeserializeOwned
202 + std::fmt::Debug
203 + Clone
204 + Send
205 + Sync
206 + for<'de> server_fn::serde::Deserialize<'de>,
207 F::Output: IntoRes<server_fn::codec::Json, BrowserMockRes, Err>
208 + FromRes<server_fn::codec::Json, OrigBrowserResponse, Err>
209 + Send
210 + for<'de> server_fn::serde::Deserialize<'de>,
211 {
212 const PATH: &'static str = F::PATH;
213 type Client = ClientWrap;
214 type Server = F::Server;
215 type Protocol = leptos::server_fn::Http<EncodingWrap<In>, server_fn::codec::Json>;
216 type Output = F::Output;
217 type Error = Err;
218 type InputStreamError = Err;
219 type OutputStreamError = Err;
220 fn middlewares() -> Vec<
221 std::sync::Arc<
222 dyn server_fn::middleware::Layer<
223 <Self::Server as server_fn::server::Server<Self::Error>>::Request,
224 <Self::Server as server_fn::server::Server<Self::Error>>::Response,
225 >,
226 >,
227 > {
228 F::middlewares()
229 }
230 async fn run_body(self) -> Result<Self::Output, Self::Error> {
231 unreachable!()
232 }
233 }
234
235 impl<E: FromServerFnError> ClientReq<E> for BrowserRequest {
236 type FormData = BrowserFormData;
237
238 fn try_new_req_query(
239 path: &str,
240 content_type: &str,
241 accepts: &str,
242 query: &str,
243 method: http::Method,
244 ) -> Result<Self, E> {
245 let mut url = String::with_capacity(path.len() + 1 + query.len());
246 url.push_str(path);
247 url.push('?');
248 url.push_str(query);
249 let inner = match method {
250 http::Method::GET => Request::get(&url),
251 http::Method::DELETE => Request::delete(&url),
252 http::Method::POST => Request::post(&url),
253 http::Method::PUT => Request::put(&url),
254 http::Method::PATCH => Request::patch(&url),
255 m => {
256 return Err(E::from_server_fn_error(
257 ServerFnErrorErr::UnsupportedRequestMethod(m.to_string()),
258 ));
259 }
260 };
261 Ok(Self(SendWrapper::new(
262 inner
263 .header("Content-Type", content_type)
264 .header("Accept", accepts)
265 .build()
266 .map_err(|e| {
267 E::from_server_fn_error(ServerFnErrorErr::Request(e.to_string()))
268 })?,
269 )))
270 }
271
272 fn try_new_req_text(
273 path: &str,
274 content_type: &str,
275 accepts: &str,
276 body: String,
277 method: http::Method,
278 ) -> Result<Self, E> {
279 let url = path;
280 let inner = match method {
281 http::Method::POST => Request::post(&url),
282 http::Method::PATCH => Request::patch(&url),
283 http::Method::PUT => Request::put(&url),
284 m => {
285 return Err(E::from_server_fn_error(
286 ServerFnErrorErr::UnsupportedRequestMethod(m.to_string()),
287 ));
288 }
289 };
290 Ok(Self(SendWrapper::new(
291 inner
292 .header("Content-Type", content_type)
293 .header("Accept", accepts)
294 .body(body)
295 .map_err(|e| {
296 E::from_server_fn_error(ServerFnErrorErr::Request(e.to_string()))
297 })?,
298 )))
299 }
300
301 fn try_new_req_bytes(
302 path: &str,
303 content_type: &str,
304 accepts: &str,
305 body: Bytes,
306 method: http::Method,
307 ) -> Result<Self, E> {
308 let url = path;
309 let body: &[u8] = &body;
310 let body = leptos::web_sys::js_sys::Uint8Array::from(body).buffer();
311 let inner = match method {
312 http::Method::POST => Request::post(&url),
313 http::Method::PATCH => Request::patch(&url),
314 http::Method::PUT => Request::put(&url),
315 m => {
316 return Err(E::from_server_fn_error(
317 ServerFnErrorErr::UnsupportedRequestMethod(m.to_string()),
318 ));
319 }
320 };
321 Ok(Self(SendWrapper::new(
322 inner
323 .header("Content-Type", content_type)
324 .header("Accept", accepts)
325 .body(body)
326 .map_err(|e| {
327 E::from_server_fn_error(ServerFnErrorErr::Request(e.to_string()))
328 })?,
329 )))
330 }
331
332 fn try_new_req_multipart(
333 path: &str,
334 accepts: &str,
335 body: Self::FormData,
336 method: http::Method,
337 ) -> Result<Self, E> {
338 let url = path;
339 let inner = match method {
340 http::Method::POST => Request::post(&url),
341 http::Method::PATCH => Request::patch(&url),
342 http::Method::PUT => Request::put(&url),
343 m => {
344 return Err(E::from_server_fn_error(
345 ServerFnErrorErr::UnsupportedRequestMethod(m.to_string()),
346 ));
347 }
348 };
349 Ok(Self(SendWrapper::new(
350 inner
351 .header("Accept", accepts)
352 .body(body.0.take())
353 .map_err(|e| {
354 E::from_server_fn_error(ServerFnErrorErr::Request(e.to_string()))
355 })?,
356 )))
357 }
358
359 fn try_new_req_form_data(
360 path: &str,
361 accepts: &str,
362 content_type: &str,
363 body: Self::FormData,
364 method: http::Method,
365 ) -> Result<Self, E> {
366 let form_data = body.0.take();
367 let url_params =
368 leptos::web_sys::UrlSearchParams::new_with_str_sequence_sequence(&form_data)
369 .map_err(|e| {
370 E::from_server_fn_error(ServerFnErrorErr::Serialization(
371 e.as_string().unwrap_or_else(|| {
372 "Could not serialize FormData to URLSearchParams".to_string()
373 }),
374 ))
375 })?;
376 let inner = match method {
377 http::Method::POST => Request::post(path),
378 http::Method::PUT => Request::put(path),
379 http::Method::PATCH => Request::patch(path),
380 m => {
381 return Err(E::from_server_fn_error(
382 ServerFnErrorErr::UnsupportedRequestMethod(m.to_string()),
383 ));
384 }
385 };
386 Ok(Self(SendWrapper::new(
387 inner
388 .header("Content-Type", content_type)
389 .header("Accept", accepts)
390 .body(url_params)
391 .map_err(|e| {
392 E::from_server_fn_error(ServerFnErrorErr::Request(e.to_string()))
393 })?,
394 )))
395 }
396
397 fn try_new_req_streaming(
398 path: &str,
399 accepts: &str,
400 content_type: &str,
401 body: impl Stream<Item = Bytes> + Send + 'static,
402 method: http::Method,
403 ) -> Result<Self, E> {
404 fn streaming_request(
405 path: &str,
406 accepts: &str,
407 content_type: &str,
408 method: http::Method,
409 body: impl Stream<Item = Bytes> + 'static,
410 ) -> Result<Request, leptos::wasm_bindgen::JsValue> {
411 use leptos::wasm_bindgen::JsValue;
412 let stream = ReadableStream::from_stream(body.map(|bytes| {
413 let data = leptos::web_sys::js_sys::Uint8Array::from(bytes.as_ref());
414 let data = JsValue::from(data);
415 Ok(data) as Result<JsValue, JsValue>
416 }))
417 .into_raw();
418
419 let headers = leptos::web_sys::Headers::new()?;
420 headers.append("Content-Type", content_type)?;
421 headers.append("Accept", accepts)?;
422
423 let init = leptos::web_sys::RequestInit::new();
424 init.set_headers(&headers);
425 init.set_method(method.as_str());
426 init.set_body(&stream);
427
428 leptos::web_sys::js_sys::Reflect::set(
430 &init,
431 &JsValue::from_str("duplex"),
432 &JsValue::from_str("half"),
433 )?;
434 let req = leptos::web_sys::Request::new_with_str_and_init(path, &init)?;
435 Ok(Request::from(req))
436 }
437
438 let request =
440 streaming_request(path, accepts, content_type, method, body).map_err(|e| {
441 E::from_server_fn_error(ServerFnErrorErr::Request(format!("{e:?}")))
442 })?;
443 Ok(Self(SendWrapper::new(request)))
444 }
445 }
446
447 struct ClientWrap;
448 impl<
449 Error: FromServerFnError + Send,
450 InputStreamError: FromServerFnError,
451 OutputStreamError: FromServerFnError,
452 > leptos::server_fn::client::Client<Error, InputStreamError, OutputStreamError> for ClientWrap
453 {
454 type Request = BrowserRequest;
455 type Response = BrowserResponse;
456
457 fn send(req: BrowserRequest) -> impl Future<Output = Result<Self::Response, Error>> + Send {
458 SendWrapper::new(async move {
459 let request = req.0.take();
460 let res = request
461 .send()
462 .await
463 .map(|res| BrowserResponse(SendWrapper::new(res)))
464 .map_err(|e| {
465 Error::from_server_fn_error(ServerFnErrorErr::Request(e.to_string()))
466 });
467 res
468 })
469 }
470 async fn open_websocket(
471 _: &str,
472 ) -> Result<
473 (
474 impl Stream<Item = Result<Bytes, Bytes>> + Send + 'static,
475 impl futures::Sink<Bytes> + Send + 'static,
476 ),
477 Error,
478 > {
479 Err::<(futures::stream::BoxStream<Result<Bytes, Bytes>>, Vec<Bytes>), _>(
480 Error::from_server_fn_error(ServerFnErrorErr::ServerError(
481 "Not implemented".to_string(),
482 )),
483 )
484 }
485
486 fn spawn(future: impl Future<Output = ()> + Send + 'static) {
487 wasm_bindgen_futures::spawn_local(future);
488 }
489 }
490
491 struct BrowserResponse(SendWrapper<leptos::server_fn::response::browser::Response>);
492
493 impl<E: FromServerFnError> ClientRes<E> for BrowserResponse {
494 fn try_into_string(self) -> impl Future<Output = Result<String, E>> + Send {
495 SendWrapper::new(async move {
496 self.0.text().await.map_err(|e| {
497 E::from_server_fn_error(ServerFnErrorErr::Deserialization(e.to_string()))
498 })
499 })
500 }
501
502 fn try_into_bytes(self) -> impl Future<Output = Result<Bytes, E>> + Send {
503 SendWrapper::new(async move {
504 self.0.binary().await.map(Bytes::from).map_err(|e| {
505 E::from_server_fn_error(ServerFnErrorErr::Deserialization(e.to_string()))
506 })
507 })
508 }
509
510 fn try_into_stream(
511 self,
512 ) -> Result<impl Stream<Item = Result<Bytes, Bytes>> + Send + Sync + 'static, E> {
513 let stream = ReadableStream::from_raw(self.0.body().unwrap())
514 .into_stream()
515 .map(|data| match data {
516 Err(e) => {
517 leptos::web_sys::console::error_1(&e);
518 Err(format!("{e:?}").into())
519 }
520 Ok(data) => {
521 let data = data.unchecked_into::<leptos::web_sys::js_sys::Uint8Array>();
522 let mut buf = Vec::new();
523 let length = data.length();
524 buf.resize(length as usize, 0);
525 data.copy_to(&mut buf);
526 Ok(Bytes::from(buf))
527 }
528 });
529 Ok(SendWrapper::new(stream))
530 }
531
532 fn status(&self) -> u16 {
533 self.0.status()
534 }
535
536 fn status_text(&self) -> String {
537 self.0.status_text()
538 }
539
540 fn location(&self) -> String {
541 self.0
542 .headers()
543 .get("Location")
544 .unwrap_or_else(|| self.0.url())
545 }
546
547 fn has_redirect(&self) -> bool {
548 self.0
549 .headers()
550 .get(leptos::server_fn::redirect::REDIRECT_HEADER)
551 .is_some()
552 }
553 }
554
555 impl<
558 In: Encoding,
559 Err: Send + Sync + FromServerFnError,
560 F: leptos::server_fn::ServerFn<
561 Client = leptos::server_fn::client::browser::BrowserClient,
562 Server = leptos::server_fn::mock::BrowserMockServer,
563 Protocol = leptos::server_fn::Http<In, server_fn::codec::Json>,
564 Error = Err,
565 InputStreamError = Err,
566 OutputStreamError = Err,
567 >,
568 > ServerFnExt for F
569 where
570 F: FromReq<In, BrowserMockReq, Err>
571 + IntoReq<In, OrigBrowserRequest, Err>
572 + server_fn::serde::Serialize
573 + server_fn::serde::de::DeserializeOwned
574 + std::fmt::Debug
575 + Clone
576 + Send
577 + Sync
578 + for<'de> server_fn::serde::Deserialize<'de>,
579 F::Output: IntoRes<server_fn::codec::Json, BrowserMockRes, Err>
580 + FromRes<server_fn::codec::Json, OrigBrowserResponse, Err>
581 + Send
582 + for<'de> server_fn::serde::Deserialize<'de>,
583 {
584 type Output = <Self as leptos::server_fn::ServerFn>::Output;
585 type Error = <Self as leptos::server_fn::ServerFn>::Error;
586 #[cfg(feature = "hydrate")]
587 async fn call_remote(self, url: String) -> Result<Self::Output, Self::Error> {
588 use leptos::server_fn::ServerFn;
589 Paired { sfn: self, url }.run_on_client().await
590 }
591 }
592}