flams_router_base/
lib.rs

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")]
47/// #### Errors
48pub 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/*
72#[cfg(feature = "hydrate")]
73mod hydrate {
74    use super::ServerFnExt;
75    use leptos::{
76        prelude::{FromServerFnError, ServerFnError},
77        server_fn::{
78            self,
79            codec::{FromReq, FromRes, IntoReq, IntoRes},
80            request::BrowserMockReq,
81            response::BrowserMockRes,
82        },
83    };
84
85    impl<
86        In: server_fn::codec::Encoding,
87        Out: server_fn::codec::Encoding,
88        Err: server_fn::serde::Serialize
89            + server_fn::serde::de::DeserializeOwned
90            + std::fmt::Debug
91            + Clone
92            + Send
93            + Sync
94            + std::fmt::Display
95            + std::str::FromStr
96            + 'static,
97        F: leptos::server_fn::ServerFn<
98                Client = leptos::server_fn::client::browser::BrowserClient,
99                Server = leptos::server_fn::mock::BrowserMockServer,
100                Protocol = leptos::server_fn::Http<In, Out>,
101                Error = ServerFnError<Err>,
102                InputStreamError = ServerFnError<Err>,
103                OutputStreamError = ServerFnError<Err>,
104            > + FromReq<In, BrowserMockReq, F::Error>
105            + IntoReq<In, server_fn::request::browser::BrowserRequest, F::Error>
106            + server_fn::serde::Serialize
107            + server_fn::serde::de::DeserializeOwned
108            + std::fmt::Debug
109            + Clone
110            + Send
111            + Sync
112            + for<'de> server_fn::serde::Deserialize<'de>,
113    > ServerFnExt for F
114    where
115        F::Output: IntoRes<Out, BrowserMockRes, F::Error>
116            + FromRes<Out, server_fn::response::browser::BrowserResponse, F::Error>
117            + IntoReq<In, server_fn::request::browser::BrowserRequest, F::Error>,
118    {
119        type Output = <Self as leptos::server_fn::ServerFn>::Output;
120        type Error = F::Error;
121        #[cfg(feature = "hydrate")]
122        async fn call_remote(self, url: String) -> Result<Self::Output, Self::Error> {
123            use server_fn::response::ClientRes;
124            let input = self;
125            let path = format!("{}{}", url, F::PATH);
126            let path: &str = &path;
127            // ---------------------------------------
128            /*
129            Ok(<F::Protocol as server_fn::Protocol<
130                F,
131                F::Output,
132                F::Client,
133                F::Server,
134                F::Error,
135                F::Error,
136                F::Error,
137            >>::run_client(path, input)
138            .await?)
139            */
140
141            // create and send request on client
142            let req = input.into_req(path, Out::CONTENT_TYPE)?;
143            let req = TODO SOMETHING ELSE;
144            let res =
145                <leptos::server_fn::client::browser::BrowserClient as server_fn::client::Client<
146                    ServerFnError<Err>,
147                    ServerFnError<Err>,
148                    ServerFnError<Err>,
149                >>::send(req)
150                .await?;
151
152            let status = <server_fn::response::browser::BrowserResponse as ClientRes<
153                ServerFnError<Err>,
154            >>::status(&res);
155            let location = <server_fn::response::browser::BrowserResponse as ClientRes<
156                ServerFnError<Err>,
157            >>::location(&res);
158            let has_redirect_header = <server_fn::response::browser::BrowserResponse as ClientRes<
159                ServerFnError<Err>,
160            >>::has_redirect(&res);
161
162            // if it returns an error status, deserialize the error using the error's decoder.
163            let res = if (400..=599).contains(&status) {
164                let bytes = <server_fn::response::browser::BrowserResponse as ClientRes<
165                    ServerFnError<Err>,
166                >>::try_into_bytes(res);
167                Err(ServerFnError::de(bytes.await?))
168            } else {
169                // otherwise, deserialize the body as is
170                let output =
171                    <Self::Output as FromRes<Out, _, ServerFnError<Err>>>::from_res(res).await?;
172                Ok(output)
173            }?;
174
175            // if redirected, call the redirect hook (if that's been set)
176            if (300..=399).contains(&status) || has_redirect_header {
177                server_fn::redirect::call_redirect_hook(&location);
178            }
179            Ok(res)
180        }
181    }
182}
183 */
184
185#[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    // -------------------------------------
204    //
205    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>, //OutputEncoding = leptos::server_fn::codec::Json,
211            >,
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                // Chrome requires setting `duplex: "half"` on streaming requests
515                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            // TODO abort signal
525            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    // -------------------------------------
642
643    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}