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