1#![allow(clippy::must_use_candidate)]
2#![cfg_attr(docsrs, feature(doc_auto_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;
11pub use login_state::*;
12pub mod uris;
13pub mod ws;
14
15use leptos::{either::EitherOf3, prelude::*};
16
17pub fn vscode_link(
18 archive: &flams_ontology::uris::ArchiveId,
19 rel_path: &str,
20) -> impl IntoView + use<> {
21 let href = format!("vscode://kwarc.flams/open?a={archive}&rp={rel_path}");
22 view! {
23 <a href=href><thaw::Icon icon=icondata_tb::TbBrandVscodeOutline/></a>
24 }
25}
26
27#[component]
28pub fn RequireLogin<Ch: IntoView + 'static>(children: TypedChildren<Ch>) -> impl IntoView {
29 require_login(children.into_inner())
30}
31
32pub fn require_login<Ch: IntoView + 'static>(
33 children: impl FnOnce() -> Ch + Send + 'static,
34) -> impl IntoView {
35 use flams_web_utils::components::{Spinner, display_error};
36
37 let children = std::sync::Arc::new(flams_utils::parking_lot::Mutex::new(Some(children)));
38 move || match LoginState::get() {
39 LoginState::Loading => EitherOf3::A(view!(<Spinner/>)),
40 LoginState::Admin | LoginState::NoAccounts | LoginState::User { is_admin: true, .. } => {
41 EitherOf3::B((children.clone().lock().take()).map(|f| f()))
42 }
43 _ => EitherOf3::C(view!(<div>{display_error("Not logged in".into())}</div>)),
44 }
45}
46
47#[cfg(feature = "ssr")]
48pub fn get_oauth() -> Result<(flams_git::gl::auth::GitLabOAuth, String), String> {
50 use flams_git::gl::auth::GitLabOAuth;
51 use leptos::prelude::*;
52 let Some(session) = use_context::<axum_login::AuthSession<flams_database::DBBackend>>() else {
53 return Err("Internal Error".to_string());
54 };
55 let Some(user) = session.user else {
56 return Err("Not logged in".to_string());
57 };
58 let Some(oauth): Option<GitLabOAuth> = expect_context() else {
59 return Err("Not Gitlab integration set up".to_string());
60 };
61 Ok((oauth, user.secret))
62}
63
64pub trait ServerFnExt {
65 type Output;
66 type Error;
67 #[cfg(feature = "hydrate")]
68 #[allow(async_fn_in_trait)]
69 async fn call_remote(self, url: String) -> Result<Self::Output, Self::Error>;
70}
71#[cfg(feature = "hydrate")]
186mod hydrate {
187 use super::ServerFnExt;
188 use bytes::Bytes;
189 use futures::{Stream, StreamExt};
190 use leptos::server_fn::codec::{Encoding, FromReq, FromRes, IntoReq, IntoRes};
191 use leptos::server_fn::request::BrowserMockReq;
192 use leptos::server_fn::request::browser::{BrowserRequest as OrigBrowserRequest, Request};
193 use leptos::server_fn::response::BrowserMockRes;
194 use leptos::server_fn::response::browser::BrowserResponse as OrigBrowserResponse;
195 use leptos::{
196 prelude::*,
197 server_fn::{request::ClientReq, response::ClientRes},
198 wasm_bindgen::JsCast,
199 };
200 use send_wrapper::SendWrapper;
201 use wasm_streams::ReadableStream;
202
203 struct Paired<
206 In,
207 F: leptos::server_fn::ServerFn<
208 Client = leptos::server_fn::client::browser::BrowserClient,
209 Server = leptos::server_fn::mock::BrowserMockServer,
210 Protocol = leptos::server_fn::Http<In, server_fn::codec::Json>, >,
212 > {
213 sfn: F,
214 url: String,
215 }
216
217 struct BrowserRequest(SendWrapper<Request>);
218 struct BrowserFormData(SendWrapper<leptos::web_sys::FormData>);
219 struct EncodingWrap<E: Encoding>(E);
220
221 impl<E: Encoding> server_fn::ContentType for EncodingWrap<E> {
222 const CONTENT_TYPE: &'static str = E::CONTENT_TYPE;
223 }
224 impl<E: Encoding> Encoding for EncodingWrap<E> {
225 const METHOD: http::Method = E::METHOD;
226 }
227
228 impl<
229 In: Encoding,
230 Err: server_fn::serde::Serialize
231 + server_fn::serde::de::DeserializeOwned
232 + std::fmt::Debug
233 + Clone
234 + Send
235 + Sync
236 + std::fmt::Display
237 + std::str::FromStr
238 + 'static,
239 F: leptos::server_fn::ServerFn<
240 Client = leptos::server_fn::client::browser::BrowserClient,
241 Server = leptos::server_fn::mock::BrowserMockServer,
242 Protocol = leptos::server_fn::Http<In, server_fn::codec::Json>,
243 Error = ServerFnError<Err>,
244 InputStreamError = ServerFnError<Err>,
245 OutputStreamError = ServerFnError<Err>,
246 > + FromReq<In, BrowserMockReq, ServerFnError<Err>>,
247 > FromReq<EncodingWrap<In>, BrowserMockReq, ServerFnError<Err>> for Paired<In, F>
248 {
249 async fn from_req(req: BrowserMockReq) -> Result<Self, ServerFnError<Err>> {
250 Ok(Self {
251 sfn: <F as FromReq<In, BrowserMockReq, ServerFnError<Err>>>::from_req(req).await?,
252 url: String::new(),
253 })
254 }
255 }
256
257 impl<
258 In: Encoding,
259 Err: server_fn::serde::Serialize
260 + server_fn::serde::de::DeserializeOwned
261 + std::fmt::Debug
262 + Clone
263 + Send
264 + Sync
265 + std::fmt::Display
266 + std::str::FromStr
267 + 'static,
268 F: leptos::server_fn::ServerFn<
269 Client = leptos::server_fn::client::browser::BrowserClient,
270 Server = leptos::server_fn::mock::BrowserMockServer,
271 Protocol = leptos::server_fn::Http<In, server_fn::codec::Json>,
272 Error = ServerFnError<Err>,
273 InputStreamError = ServerFnError<Err>,
274 OutputStreamError = ServerFnError<Err>,
275 > + IntoReq<In, OrigBrowserRequest, ServerFnError<Err>>,
276 > IntoReq<EncodingWrap<In>, BrowserRequest, ServerFnError<Err>> for Paired<In, F>
277 {
278 fn into_req(
279 self,
280 _path: &str,
281 accepts: &str,
282 ) -> Result<BrowserRequest, ServerFnError<Err>> {
283 let Paired { sfn, url } = self;
284 let path = format!("{}{}", url, F::PATH);
285 let req = <F as IntoReq<In, OrigBrowserRequest, _>>::into_req(sfn, &path, accepts)?;
286 let req: Request = req.into();
287 Ok(BrowserRequest(SendWrapper::new(req)))
288 }
289 }
290
291 impl<
292 In: Encoding,
293 Err: server_fn::serde::Serialize
294 + server_fn::serde::de::DeserializeOwned
295 + std::fmt::Debug
296 + Clone
297 + Send
298 + Sync
299 + std::fmt::Display
300 + std::str::FromStr
301 + 'static,
302 F: leptos::server_fn::ServerFn<
303 Client = leptos::server_fn::client::browser::BrowserClient,
304 Server = leptos::server_fn::mock::BrowserMockServer,
305 Protocol = leptos::server_fn::Http<In, server_fn::codec::Json>,
306 Error = ServerFnError<Err>,
307 InputStreamError = ServerFnError<Err>,
308 OutputStreamError = ServerFnError<Err>,
309 >,
310 > leptos::server_fn::ServerFn for Paired<In, F>
311 where
312 F: FromReq<In, BrowserMockReq, ServerFnError<Err>>
313 + IntoReq<In, OrigBrowserRequest, ServerFnError<Err>>
314 + server_fn::serde::Serialize
315 + server_fn::serde::de::DeserializeOwned
316 + std::fmt::Debug
317 + Clone
318 + Send
319 + Sync
320 + for<'de> server_fn::serde::Deserialize<'de>,
321 F::Output: IntoRes<server_fn::codec::Json, BrowserMockRes, ServerFnError<Err>>
325 + FromRes<server_fn::codec::Json, OrigBrowserResponse, ServerFnError<Err>>
326 + Send
327 + for<'de> server_fn::serde::Deserialize<'de>,
328 {
329 const PATH: &'static str = F::PATH;
330 type Client = ClientWrap;
331 type Server = F::Server;
332 type Protocol = leptos::server_fn::Http<EncodingWrap<In>, server_fn::codec::Json>;
333 type Output = F::Output;
334 type Error = ServerFnError<Err>;
335 type InputStreamError = ServerFnError<Err>;
336 type OutputStreamError = ServerFnError<Err>;
337 fn middlewares() -> Vec<
338 std::sync::Arc<
339 dyn server_fn::middleware::Layer<
340 <Self::Server as server_fn::server::Server<Self::Error>>::Request,
341 <Self::Server as server_fn::server::Server<Self::Error>>::Response,
342 >,
343 >,
344 > {
345 F::middlewares()
346 }
347 async fn run_body(self) -> Result<Self::Output, Self::Error> {
348 unreachable!()
349 }
350 }
351
352 impl<E: FromServerFnError> ClientReq<E> for BrowserRequest {
353 type FormData = BrowserFormData;
354
355 fn try_new_req_query(
356 path: &str,
357 content_type: &str,
358 accepts: &str,
359 query: &str,
360 method: http::Method,
361 ) -> Result<Self, E> {
362 let mut url = String::with_capacity(path.len() + 1 + query.len());
363 url.push_str(path);
364 url.push('?');
365 url.push_str(query);
366 let inner = match method {
367 http::Method::GET => Request::get(&url),
368 http::Method::DELETE => Request::delete(&url),
369 http::Method::POST => Request::post(&url),
370 http::Method::PUT => Request::put(&url),
371 http::Method::PATCH => Request::patch(&url),
372 m => {
373 return Err(E::from_server_fn_error(
374 ServerFnErrorErr::UnsupportedRequestMethod(m.to_string()),
375 ));
376 }
377 };
378 Ok(Self(SendWrapper::new(
379 inner
380 .header("Content-Type", content_type)
381 .header("Accept", accepts)
382 .build()
383 .map_err(|e| {
384 E::from_server_fn_error(ServerFnErrorErr::Request(e.to_string()))
385 })?,
386 )))
387 }
388
389 fn try_new_req_text(
390 path: &str,
391 content_type: &str,
392 accepts: &str,
393 body: String,
394 method: http::Method,
395 ) -> Result<Self, E> {
396 let url = path;
397 let inner = match method {
398 http::Method::POST => Request::post(&url),
399 http::Method::PATCH => Request::patch(&url),
400 http::Method::PUT => Request::put(&url),
401 m => {
402 return Err(E::from_server_fn_error(
403 ServerFnErrorErr::UnsupportedRequestMethod(m.to_string()),
404 ));
405 }
406 };
407 Ok(Self(SendWrapper::new(
408 inner
409 .header("Content-Type", content_type)
410 .header("Accept", accepts)
411 .body(body)
412 .map_err(|e| {
413 E::from_server_fn_error(ServerFnErrorErr::Request(e.to_string()))
414 })?,
415 )))
416 }
417
418 fn try_new_req_bytes(
419 path: &str,
420 content_type: &str,
421 accepts: &str,
422 body: Bytes,
423 method: http::Method,
424 ) -> Result<Self, E> {
425 let url = path;
426 let body: &[u8] = &body;
427 let body = leptos::web_sys::js_sys::Uint8Array::from(body).buffer();
428 let inner = match method {
429 http::Method::POST => Request::post(&url),
430 http::Method::PATCH => Request::patch(&url),
431 http::Method::PUT => Request::put(&url),
432 m => {
433 return Err(E::from_server_fn_error(
434 ServerFnErrorErr::UnsupportedRequestMethod(m.to_string()),
435 ));
436 }
437 };
438 Ok(Self(SendWrapper::new(
439 inner
440 .header("Content-Type", content_type)
441 .header("Accept", accepts)
442 .body(body)
443 .map_err(|e| {
444 E::from_server_fn_error(ServerFnErrorErr::Request(e.to_string()))
445 })?,
446 )))
447 }
448
449 fn try_new_req_multipart(
450 path: &str,
451 accepts: &str,
452 body: Self::FormData,
453 method: http::Method,
454 ) -> Result<Self, E> {
455 let url = path;
456 let inner = match method {
457 http::Method::POST => Request::post(&url),
458 http::Method::PATCH => Request::patch(&url),
459 http::Method::PUT => Request::put(&url),
460 m => {
461 return Err(E::from_server_fn_error(
462 ServerFnErrorErr::UnsupportedRequestMethod(m.to_string()),
463 ));
464 }
465 };
466 Ok(Self(SendWrapper::new(
467 inner
468 .header("Accept", accepts)
469 .body(body.0.take())
470 .map_err(|e| {
471 E::from_server_fn_error(ServerFnErrorErr::Request(e.to_string()))
472 })?,
473 )))
474 }
475
476 fn try_new_req_form_data(
477 path: &str,
478 accepts: &str,
479 content_type: &str,
480 body: Self::FormData,
481 method: http::Method,
482 ) -> Result<Self, E> {
483 let form_data = body.0.take();
484 let url_params =
485 leptos::web_sys::UrlSearchParams::new_with_str_sequence_sequence(&form_data)
486 .map_err(|e| {
487 E::from_server_fn_error(ServerFnErrorErr::Serialization(
488 e.as_string().unwrap_or_else(|| {
489 "Could not serialize FormData to URLSearchParams".to_string()
490 }),
491 ))
492 })?;
493 let inner = match method {
494 http::Method::POST => Request::post(path),
495 http::Method::PUT => Request::put(path),
496 http::Method::PATCH => Request::patch(path),
497 m => {
498 return Err(E::from_server_fn_error(
499 ServerFnErrorErr::UnsupportedRequestMethod(m.to_string()),
500 ));
501 }
502 };
503 Ok(Self(SendWrapper::new(
504 inner
505 .header("Content-Type", content_type)
506 .header("Accept", accepts)
507 .body(url_params)
508 .map_err(|e| {
509 E::from_server_fn_error(ServerFnErrorErr::Request(e.to_string()))
510 })?,
511 )))
512 }
513
514 fn try_new_req_streaming(
515 path: &str,
516 accepts: &str,
517 content_type: &str,
518 body: impl Stream<Item = Bytes> + Send + 'static,
519 method: http::Method,
520 ) -> Result<Self, E> {
521 fn streaming_request(
522 path: &str,
523 accepts: &str,
524 content_type: &str,
525 method: http::Method,
526 body: impl Stream<Item = Bytes> + 'static,
527 ) -> Result<Request, leptos::wasm_bindgen::JsValue> {
528 use leptos::wasm_bindgen::JsValue;
529 let stream = ReadableStream::from_stream(body.map(|bytes| {
530 let data = leptos::web_sys::js_sys::Uint8Array::from(bytes.as_ref());
531 let data = JsValue::from(data);
532 Ok(data) as Result<JsValue, JsValue>
533 }))
534 .into_raw();
535
536 let headers = leptos::web_sys::Headers::new()?;
537 headers.append("Content-Type", content_type)?;
538 headers.append("Accept", accepts)?;
539
540 let init = leptos::web_sys::RequestInit::new();
541 init.set_headers(&headers);
542 init.set_method(method.as_str());
543 init.set_body(&stream);
544
545 leptos::web_sys::js_sys::Reflect::set(
547 &init,
548 &JsValue::from_str("duplex"),
549 &JsValue::from_str("half"),
550 )?;
551 let req = leptos::web_sys::Request::new_with_str_and_init(path, &init)?;
552 Ok(Request::from(req))
553 }
554
555 let request =
557 streaming_request(path, accepts, content_type, method, body).map_err(|e| {
558 E::from_server_fn_error(ServerFnErrorErr::Request(format!("{e:?}")))
559 })?;
560 Ok(Self(SendWrapper::new(request)))
561 }
562 }
563
564 struct ClientWrap;
565 impl<
566 Error: FromServerFnError + Send,
567 InputStreamError: FromServerFnError,
568 OutputStreamError: FromServerFnError,
569 > leptos::server_fn::client::Client<Error, InputStreamError, OutputStreamError> for ClientWrap
570 {
571 type Request = BrowserRequest;
572 type Response = BrowserResponse;
573
574 fn send(req: BrowserRequest) -> impl Future<Output = Result<Self::Response, Error>> + Send {
575 SendWrapper::new(async move {
576 let request = req.0.take();
577 let res = request
578 .send()
579 .await
580 .map(|res| BrowserResponse(SendWrapper::new(res)))
581 .map_err(|e| {
582 Error::from_server_fn_error(ServerFnErrorErr::Request(e.to_string()))
583 });
584 res
585 })
586 }
587 async fn open_websocket(
588 _: &str,
589 ) -> Result<
590 (
591 impl Stream<Item = Result<Bytes, Bytes>> + Send + 'static,
592 impl futures::Sink<Bytes> + Send + 'static,
593 ),
594 Error,
595 > {
596 Err::<(futures::stream::BoxStream<Result<Bytes, Bytes>>, Vec<Bytes>), _>(
597 Error::from_server_fn_error(ServerFnErrorErr::ServerError(
598 "Not implemented".to_string(),
599 )),
600 )
601 }
602
603 fn spawn(future: impl Future<Output = ()> + Send + 'static) {
604 wasm_bindgen_futures::spawn_local(future);
605 }
606 }
607
608 struct BrowserResponse(SendWrapper<leptos::server_fn::response::browser::Response>);
609
610 impl<E: FromServerFnError> ClientRes<E> for BrowserResponse {
611 fn try_into_string(self) -> impl Future<Output = Result<String, E>> + Send {
612 SendWrapper::new(async move {
613 self.0.text().await.map_err(|e| {
614 E::from_server_fn_error(ServerFnErrorErr::Deserialization(e.to_string()))
615 })
616 })
617 }
618
619 fn try_into_bytes(self) -> impl Future<Output = Result<Bytes, E>> + Send {
620 SendWrapper::new(async move {
621 self.0.binary().await.map(Bytes::from).map_err(|e| {
622 E::from_server_fn_error(ServerFnErrorErr::Deserialization(e.to_string()))
623 })
624 })
625 }
626
627 fn try_into_stream(
628 self,
629 ) -> Result<impl Stream<Item = Result<Bytes, Bytes>> + Send + Sync + 'static, E> {
630 let stream = ReadableStream::from_raw(self.0.body().unwrap())
631 .into_stream()
632 .map(|data| match data {
633 Err(e) => {
634 leptos::web_sys::console::error_1(&e);
635 Err(format!("{e:?}").into())
636 }
637 Ok(data) => {
638 let data = data.unchecked_into::<leptos::web_sys::js_sys::Uint8Array>();
639 let mut buf = Vec::new();
640 let length = data.length();
641 buf.resize(length as usize, 0);
642 data.copy_to(&mut buf);
643 Ok(Bytes::from(buf))
644 }
645 });
646 Ok(SendWrapper::new(stream))
647 }
648
649 fn status(&self) -> u16 {
650 self.0.status()
651 }
652
653 fn status_text(&self) -> String {
654 self.0.status_text()
655 }
656
657 fn location(&self) -> String {
658 self.0
659 .headers()
660 .get("Location")
661 .unwrap_or_else(|| self.0.url())
662 }
663
664 fn has_redirect(&self) -> bool {
665 self.0
666 .headers()
667 .get(leptos::server_fn::redirect::REDIRECT_HEADER)
668 .is_some()
669 }
670 }
671
672 impl<
675 In: Encoding,
676 Err: server_fn::serde::Serialize
677 + server_fn::serde::de::DeserializeOwned
678 + std::fmt::Debug
679 + Clone
680 + Send
681 + Sync
682 + std::fmt::Display
683 + std::str::FromStr
684 + 'static,
685 F: leptos::server_fn::ServerFn<
686 Client = leptos::server_fn::client::browser::BrowserClient,
687 Server = leptos::server_fn::mock::BrowserMockServer,
688 Protocol = leptos::server_fn::Http<In, server_fn::codec::Json>,
689 Error = ServerFnError<Err>,
690 InputStreamError = ServerFnError<Err>,
691 OutputStreamError = ServerFnError<Err>,
692 >,
693 > ServerFnExt for F
694 where
695 F: FromReq<In, BrowserMockReq, ServerFnError<Err>>
696 + IntoReq<In, OrigBrowserRequest, ServerFnError<Err>>
697 + server_fn::serde::Serialize
698 + server_fn::serde::de::DeserializeOwned
699 + std::fmt::Debug
700 + Clone
701 + Send
702 + Sync
703 + for<'de> server_fn::serde::Deserialize<'de>,
704 F::Output: IntoRes<server_fn::codec::Json, BrowserMockRes, ServerFnError<Err>>
705 + FromRes<server_fn::codec::Json, OrigBrowserResponse, ServerFnError<Err>>
706 + Send
707 + for<'de> server_fn::serde::Deserialize<'de>,
708 {
709 type Output = <Self as leptos::server_fn::ServerFn>::Output;
710 type Error = <Self as leptos::server_fn::ServerFn>::Error;
711 #[cfg(feature = "hydrate")]
712 async fn call_remote(self, url: String) -> Result<Self::Output, Self::Error> {
713 use leptos::server_fn::ServerFn;
714 Paired { sfn: self, url }.run_on_client().await
715 }
716 }
717}