leptos_use/
use_cookie.rs

1#![allow(clippy::too_many_arguments)]
2
3use crate::core::now;
4use crate::utils::get_header;
5use codee::{CodecError, Decoder, Encoder};
6use cookie::time::{Duration, OffsetDateTime};
7pub use cookie::SameSite;
8use cookie::{Cookie, CookieJar};
9use default_struct_builder::DefaultBuilder;
10use leptos::{
11    logging::{debug_warn, error},
12    prelude::*,
13};
14use std::sync::Arc;
15
16/// SSR-friendly and reactive cookie access.
17///
18/// You can use this function multiple times for the same cookie and their signals will synchronize
19/// (even across windows/tabs). But there is no way to listen to changes to `document.cookie` directly so in case
20/// something outside of this function changes the cookie, the signal will **not** be updated.
21///
22/// When the options `max_age` or `expire` is given then the returned signal will
23/// automatically turn to `None` after that time.
24///
25/// ## Demo
26///
27/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_cookie)
28///
29/// ## Usage
30///
31/// The example below creates a cookie called `counter`. If the cookie doesn't exist, it is initially set to a random value.
32/// Whenever we update the `counter` variable, the cookie will be updated accordingly.
33///
34/// ```
35/// # use leptos::prelude::*;
36/// # use leptos_use::use_cookie;
37/// # use codee::string::FromToStringCodec;
38/// # use rand::prelude::*;
39///
40/// #
41/// # #[component]
42/// # fn Demo() -> impl IntoView {
43/// let (counter, set_counter) = use_cookie::<u32, FromToStringCodec>("counter");
44///
45/// let reset = move || set_counter.set(Some(random()));
46///
47/// if counter.get().is_none() {
48///     reset();
49/// }
50///
51/// let increase = move || {
52///     set_counter.set(counter.get().map(|c| c + 1));
53/// };
54///
55/// view! {
56///     <p>Counter: {move || counter.get().map(|c| c.to_string()).unwrap_or("—".to_string())}</p>
57///     <button on:click=move |_| reset()>Reset</button>
58///     <button on:click=move |_| increase()>+</button>
59/// }
60/// # }
61/// ```
62///
63/// Values are (en)decoded via the given codec. You can use any of the string codecs or a
64/// binary codec wrapped in `Base64`.
65///
66/// > Please check [the codec chapter](https://leptos-use.rs/codecs.html) to see what codecs are
67/// > available and what feature flags they require.
68///
69/// ## Cookie attributes
70///
71/// As part of the options when you use `use_cookie_with_options` you can specify cookie attributes.
72///
73/// ```
74/// # use cookie::SameSite;
75/// # use leptos::prelude::*;
76/// # use leptos_use::{use_cookie_with_options, UseCookieOptions};
77/// # use codee::string::FromToStringCodec;
78/// #
79/// # #[component]
80/// # fn Demo() -> impl IntoView {
81/// let (cookie, set_cookie) = use_cookie_with_options::<bool, FromToStringCodec>(
82///     "user_info",
83///     UseCookieOptions::default()
84///         .max_age(3600_000) // one hour
85///         .same_site(SameSite::Lax)
86/// );
87/// #
88/// # view! {}
89/// # }
90/// ```
91///
92/// ## Server-Side Rendering
93///
94/// This works equally well on the server or the client.
95/// On the server this function reads the cookie from the HTTP request header and writes it back into
96/// the HTTP response header according to options (if provided).
97/// The returned `WriteSignal` may not affect the cookie headers on the server! It will try and write
98/// the headers buy if this happens after the headers have already been streamed to the client then
99/// this will have no effect.
100///
101/// > If you're using `axum` you have to enable the `"axum"` feature in your Cargo.toml.
102/// > In case it's `actix-web` enable the feature `"actix"`, for `spin` enable `"spin"`.
103///
104/// ### Bring your own header
105///
106/// In case you're neither using Axum, Actix nor Spin, or the default implementation is not to your liking,
107/// you can provide your own way of reading and writing the cookie header value.
108///
109/// ```
110/// # use cookie::Cookie;
111/// # use leptos::prelude::*;
112/// # use serde::{Deserialize, Serialize};
113/// # use leptos_use::{use_cookie_with_options, UseCookieOptions};
114/// # use codee::string::JsonSerdeCodec;
115/// #
116/// # #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
117/// # pub struct Auth {
118/// #     pub username: String,
119/// #     pub token: String,
120/// # }
121/// #
122/// # #[component]
123/// # fn Demo() -> impl IntoView {
124/// use_cookie_with_options::<Auth, JsonSerdeCodec>(
125///     "auth",
126///     UseCookieOptions::default()
127///         .ssr_cookies_header_getter(|| {
128///             #[cfg(feature = "ssr")]
129///             {
130///                 Some("Somehow get the value of the cookie header as a string".to_owned())
131///             }
132///             #[cfg(not(feature = "ssr"))]
133///             None
134///         })
135///         .ssr_set_cookie(|cookie: &Cookie| {
136///             #[cfg(feature = "ssr")]
137///             {
138///                 // somehow insert the Set-Cookie header for this cookie
139///             }
140///         }),
141/// );
142/// # view! {}
143/// # }
144/// ```
145pub fn use_cookie<T, C>(cookie_name: &str) -> (Signal<Option<T>>, WriteSignal<Option<T>>)
146where
147    C: Encoder<T, Encoded = String> + Decoder<T, Encoded = str>,
148    T: Clone + Send + Sync + 'static,
149{
150    use_cookie_with_options::<T, C>(cookie_name, UseCookieOptions::default())
151}
152
153/// Version of [`use_cookie`] that takes [`UseCookieOptions`].
154pub fn use_cookie_with_options<T, C>(
155    cookie_name: &str,
156    options: UseCookieOptions<T, <C as Encoder<T>>::Error, <C as Decoder<T>>::Error>,
157) -> (Signal<Option<T>>, WriteSignal<Option<T>>)
158where
159    C: Encoder<T, Encoded = String> + Decoder<T, Encoded = str>,
160    T: Clone + Send + Sync + 'static,
161{
162    let UseCookieOptions {
163        max_age,
164        expires,
165        http_only,
166        secure,
167        domain,
168        path,
169        same_site,
170        ssr_cookies_header_getter,
171        ssr_set_cookie,
172        default_value,
173        readonly,
174        on_error,
175    } = options;
176
177    let delay = if let Some(max_age) = max_age {
178        Some(max_age)
179    } else {
180        expires.map(|expires| expires * 1000 - now() as i64)
181    };
182
183    let has_expired = if let Some(delay) = delay {
184        delay <= 0
185    } else {
186        false
187    };
188
189    let (cookie, set_cookie) = signal(None::<T>);
190
191    let jar = StoredValue::new(CookieJar::new());
192
193    if !has_expired {
194        let ssr_cookies_header_getter = Arc::clone(&ssr_cookies_header_getter);
195
196        let new_cookie = jar.try_update_value(|jar| {
197            *jar = load_and_parse_cookie_jar(ssr_cookies_header_getter)?;
198            jar.get(cookie_name)
199                .and_then(|c| {
200                    C::decode(c.value())
201                        .map_err(|err| on_error(CodecError::Decode(err)))
202                        .ok()
203                })
204                .or(default_value)
205        });
206
207        set_cookie.set(new_cookie.flatten());
208
209        handle_expiration(delay, set_cookie);
210    } else {
211        debug_warn!(
212            "not setting cookie '{}' because it has already expired",
213            cookie_name
214        );
215    }
216
217    #[cfg(not(feature = "ssr"))]
218    {
219        use crate::{
220            use_broadcast_channel, watch_pausable, UseBroadcastChannelReturn, WatchPausableReturn,
221        };
222        use codee::string::{FromToStringCodec, OptionCodec};
223
224        let UseBroadcastChannelReturn { message, post, .. } =
225            use_broadcast_channel::<Option<String>, OptionCodec<FromToStringCodec>>(&format!(
226                "leptos-use:cookies:{cookie_name}"
227            ));
228
229        let on_cookie_change = {
230            let cookie_name = cookie_name.to_owned();
231            let ssr_cookies_header_getter = Arc::clone(&ssr_cookies_header_getter);
232            let on_error = Arc::clone(&on_error);
233            let domain = domain.clone();
234            let path = path.clone();
235
236            move || {
237                if readonly {
238                    return;
239                }
240
241                let value = cookie.try_with_untracked(|cookie| {
242                    cookie.as_ref().and_then(|cookie| {
243                        C::encode(cookie)
244                            .map_err(|err| on_error(CodecError::Encode(err)))
245                            .ok()
246                    })
247                });
248
249                if let Some(value) = value {
250                    if value
251                        == jar.with_value(|jar| jar.get(&cookie_name).map(|c| c.value().to_owned()))
252                    {
253                        return;
254                    }
255
256                    jar.update_value(|jar| {
257                        write_client_cookie(
258                            &cookie_name,
259                            &value,
260                            jar,
261                            max_age,
262                            expires,
263                            &domain,
264                            &path,
265                            same_site,
266                            secure,
267                            http_only,
268                            Arc::clone(&ssr_cookies_header_getter),
269                        );
270                    });
271
272                    post(&value);
273                }
274            }
275        };
276
277        let WatchPausableReturn {
278            pause,
279            resume,
280            stop,
281            ..
282        } = watch_pausable(move || cookie.track(), {
283            let on_cookie_change = on_cookie_change.clone();
284
285            move |_, _, _| {
286                on_cookie_change();
287            }
288        });
289
290        // listen to cookie changes from the broadcast channel
291        Effect::new({
292            let ssr_cookies_header_getter = Arc::clone(&ssr_cookies_header_getter);
293            let cookie_name = cookie_name.to_owned();
294
295            move |_| {
296                if let Some(message) = message.get() {
297                    pause();
298
299                    if let Some(message) = message {
300                        match C::decode(&message) {
301                            Ok(value) => {
302                                let ssr_cookies_header_getter =
303                                    Arc::clone(&ssr_cookies_header_getter);
304
305                                jar.update_value(|jar| {
306                                    update_client_cookie_jar(
307                                        &cookie_name,
308                                        &Some(message),
309                                        jar,
310                                        max_age,
311                                        expires,
312                                        &domain,
313                                        &path,
314                                        same_site,
315                                        secure,
316                                        http_only,
317                                        ssr_cookies_header_getter,
318                                    );
319                                });
320
321                                set_cookie.set(Some(value));
322                            }
323                            Err(err) => {
324                                on_error(CodecError::Decode(err));
325                            }
326                        }
327                    } else {
328                        let cookie_name = cookie_name.clone();
329                        let ssr_cookies_header_getter = Arc::clone(&ssr_cookies_header_getter);
330
331                        jar.update_value(|jar| {
332                            update_client_cookie_jar(
333                                &cookie_name,
334                                &None,
335                                jar,
336                                max_age,
337                                expires,
338                                &domain,
339                                &path,
340                                same_site,
341                                secure,
342                                http_only,
343                                ssr_cookies_header_getter,
344                            );
345                            jar.force_remove(cookie_name);
346                        });
347
348                        set_cookie.set(None);
349                    }
350
351                    resume();
352                }
353            }
354        });
355
356        on_cleanup(move || {
357            stop();
358            on_cookie_change();
359        });
360
361        let _ = ssr_set_cookie;
362    }
363
364    #[cfg(feature = "ssr")]
365    {
366        if !readonly {
367            Effect::new_isomorphic({
368                let cookie_name = cookie_name.to_owned();
369                let ssr_set_cookie = Arc::clone(&ssr_set_cookie);
370
371                move |previous_effect_value: Option<()>| {
372                    let domain = domain.clone();
373                    let path = path.clone();
374
375                    if let Some(value) = cookie.try_with(|cookie| {
376                        cookie.as_ref().map(|cookie| {
377                            C::encode(cookie)
378                                .map_err(|err| on_error(CodecError::Encode(err)))
379                                .ok()
380                        })
381                    }) {
382                        if previous_effect_value.is_some() {
383                            jar.update_value({
384                                let domain = domain.clone();
385                                let path = path.clone();
386                                let ssr_set_cookie = Arc::clone(&ssr_set_cookie);
387
388                                |jar| {
389                                    write_server_cookie(
390                                        &cookie_name,
391                                        value.flatten(),
392                                        jar,
393                                        max_age,
394                                        expires,
395                                        domain,
396                                        path,
397                                        same_site,
398                                        secure,
399                                        http_only,
400                                        ssr_set_cookie,
401                                    )
402                                }
403                            });
404                        }
405                    }
406
407                    ()
408                }
409            });
410        }
411    }
412
413    (cookie.into(), set_cookie)
414}
415
416/// Options for [`use_cookie_with_options`].
417#[derive(DefaultBuilder)]
418pub struct UseCookieOptions<T, E, D> {
419    /// [`Max-Age` of the cookie](https://tools.ietf.org/html/rfc6265#section-5.2.2) in milliseconds. The returned signal will turn to `None` after the max age is reached.
420    /// Default: `None`
421    ///
422    /// > The [cookie storage model specification](https://tools.ietf.org/html/rfc6265#section-5.3) states
423    /// > that if both `expires` and `max_age` is set, then `max_age` takes precedence,
424    /// > but not all clients may obey this, so if both are set, they should point to the same date and time!
425    ///
426    /// > If neither of `expires` and `max_age` is set, the cookie will be session-only and removed when the user closes their browser.
427    #[builder(into)]
428    max_age: Option<i64>,
429
430    /// [Expiration date-time of the cookie](https://tools.ietf.org/html/rfc6265#section-5.2.1) as UNIX timestamp in seconds.
431    /// The signal will turn to `None` after the expiration date-time is reached.
432    /// Default: `None`
433    ///
434    /// > The [cookie storage model specification](https://tools.ietf.org/html/rfc6265#section-5.3) states
435    /// > that if both `expires` and `max_age` is set, then `max_age` takes precedence,
436    /// > but not all clients may obey this, so if both are set, they should point to the same date and time!
437    ///
438    /// > If neither of `expires` and `max_age` is set, the cookie will be session-only and removed when the user closes their browser.
439    #[builder(into)]
440    expires: Option<i64>,
441
442    /// Specifies the [`HttpOnly` cookie attribute](https://tools.ietf.org/html/rfc6265#section-5.2.6).
443    /// When `true`, the `HttpOnly` attribute is set; otherwise it is not.
444    /// By default, the `HttpOnly` attribute is not set.
445    ///
446    /// > Be careful when setting this to `true`, as compliant clients will not allow client-side JavaScript to see the cookie in `document.cookie`.
447    http_only: bool,
448
449    /// Specifies the value for the [`Secure` cookie attribute](https://tools.ietf.org/html/rfc6265#section-5.2.5).
450    /// When `true`, the `Secure` attribute is set; otherwise it is not.
451    /// By default, the `Secure` attribute is not set.
452    ///
453    /// > Be careful when setting this to `true`, as compliant clients will not send the cookie back to the
454    /// > server in the future if the browser does not have an HTTPS connection. This can lead to hydration errors.
455    secure: bool,
456
457    /// Specifies the value for the [`Domain` cookie attribute](https://tools.ietf.org/html/rfc6265#section-5.2.3).
458    /// By default, no domain is set, and most clients will consider applying the cookie only to the current domain.
459    #[builder(into)]
460    domain: Option<String>,
461
462    /// Specifies the value for the [`Path` cookie attribute](https://tools.ietf.org/html/rfc6265#section-5.2.4).
463    /// By default, the path is considered the ["default path"](https://tools.ietf.org/html/rfc6265#section-5.1.4).
464    #[builder(into)]
465    path: Option<String>,
466
467    /// Specifies the value for the [`SameSite` cookie attribute](https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7).
468    ///
469    /// - `'Some(SameSite::Lax)'` will set the `SameSite` attribute to `Lax` for lax same-site enforcement.
470    /// - `'Some(SameSite::None)'` will set the `SameSite` attribute to `None` for an explicit cross-site cookie.
471    /// - `'Some(SameSite::Strict)'` will set the `SameSite` attribute to `Strict` for strict same-site enforcement.
472    /// - `None` will not set the `SameSite` attribute (default).
473    ///
474    /// More information about the different enforcement levels can be found in [the specification](https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7).
475    #[builder(into)]
476    same_site: Option<SameSite>,
477
478    /// The default cookie value in case the cookie is not set.
479    /// Defaults to `None`.
480    default_value: Option<T>,
481
482    /// If `true` the returned `WriteSignal` will not affect the actual cookie.
483    /// Default: `false`
484    readonly: bool,
485
486    /// Getter function to return the string value of the cookie header.
487    /// When you use one of the features `"axum"`, `"actix"` or `"spin"` there's a valid default implementation provided.
488    ssr_cookies_header_getter: Arc<dyn Fn() -> Option<String> + Send + Sync>,
489
490    /// Function to add a set cookie header to the response on the server.
491    /// When you use one of the features `"axum"`, `"actix"` or `"spin"` there's a valid default implementation provided.
492    ssr_set_cookie: Arc<dyn Fn(&Cookie) + Send + Sync>,
493
494    /// Callback for encoding/decoding errors. Defaults to logging the error to the console.
495    on_error: Arc<dyn Fn(CodecError<E, D>) + Send + Sync>,
496}
497
498impl<T, E, D> Default for UseCookieOptions<T, E, D> {
499    #[allow(dead_code)]
500    fn default() -> Self {
501        Self {
502            max_age: None,
503            expires: None,
504            http_only: false,
505            default_value: None,
506            readonly: false,
507            secure: false,
508            domain: None,
509            path: None,
510            same_site: None,
511            ssr_cookies_header_getter: Arc::new(move || {
512                get_header!(COOKIE, use_cookie, ssr_cookies_header_getter)
513            }),
514            ssr_set_cookie: Arc::new(|cookie: &Cookie| {
515                #[cfg(feature = "ssr")]
516                {
517                    #[cfg(feature = "actix")]
518                    use leptos_actix::ResponseOptions;
519                    #[cfg(feature = "axum")]
520                    use leptos_axum::ResponseOptions;
521                    #[cfg(feature = "spin")]
522                    use leptos_spin::ResponseOptions;
523
524                    #[cfg(feature = "actix")]
525                    const SET_COOKIE: http0_2::HeaderName = http0_2::header::SET_COOKIE;
526                    #[cfg(any(feature = "axum", feature = "spin"))]
527                    const SET_COOKIE: http1::HeaderName = http1::header::SET_COOKIE;
528
529                    #[cfg(feature = "actix")]
530                    type HeaderValue = http0_2::HeaderValue;
531                    #[cfg(any(feature = "axum", feature = "spin"))]
532                    type HeaderValue = http1::HeaderValue;
533
534                    #[cfg(all(
535                        not(feature = "axum"),
536                        not(feature = "actix"),
537                        not(feature = "spin")
538                    ))]
539                    {
540                        use leptos::logging::warn;
541                        let _ = cookie;
542                        warn!("If you're using use_cookie without the feature `axum`, `actix` or `spin` enabled, you should provide the option `ssr_set_cookie`");
543                    }
544
545                    #[cfg(any(feature = "axum", feature = "actix"))]
546                    {
547                        if let Some(response_options) = use_context::<ResponseOptions>() {
548                            if let Ok(header_value) =
549                                HeaderValue::from_str(&cookie.encoded().to_string())
550                            {
551                                response_options.append_header(SET_COOKIE, header_value);
552                            }
553                        }
554                    }
555                    #[cfg(feature = "spin")]
556                    {
557                        if let Some(response_options) = use_context::<ResponseOptions>() {
558                            let header_value = cookie.encoded().to_string().as_bytes().to_vec();
559                            response_options.append_header(SET_COOKIE.as_str(), &header_value);
560                        }
561                    }
562                }
563
564                let _ = cookie;
565            }),
566            on_error: Arc::new(|_| {
567                error!("cookie (de-/)serialization error");
568            }),
569        }
570    }
571}
572
573fn read_cookies_string(
574    ssr_cookies_header_getter: Arc<dyn Fn() -> Option<String> + Send + Sync>,
575) -> Option<String> {
576    let cookies;
577
578    #[cfg(feature = "ssr")]
579    {
580        cookies = ssr_cookies_header_getter();
581    }
582
583    #[cfg(not(feature = "ssr"))]
584    {
585        use wasm_bindgen::JsCast;
586
587        let _ = ssr_cookies_header_getter;
588
589        let js_value: wasm_bindgen::JsValue = document().into();
590        let document: web_sys::HtmlDocument = js_value.unchecked_into();
591        cookies = Some(document.cookie().unwrap_or_default());
592    }
593
594    cookies
595}
596
597fn handle_expiration<T>(delay: Option<i64>, set_cookie: WriteSignal<Option<T>>)
598where
599    T: Send + Sync + 'static,
600{
601    if let Some(delay) = delay {
602        #[cfg(not(feature = "ssr"))]
603        {
604            use leptos::leptos_dom::helpers::TimeoutHandle;
605            use std::sync::{atomic::AtomicI32, Mutex};
606
607            // The maximum value allowed on a timeout delay.
608            // Reference: https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#maximum_delay_value
609            const MAX_TIMEOUT_DELAY: i64 = 2_147_483_647;
610
611            let timeout = Arc::new(Mutex::new(None::<TimeoutHandle>));
612            let elapsed = Arc::new(AtomicI32::new(0));
613
614            on_cleanup({
615                let timeout = Arc::clone(&timeout);
616                move || {
617                    if let Some(timeout) = timeout.lock().unwrap().take() {
618                        timeout.clear();
619                    }
620                }
621            });
622
623            let create_expiration_timeout =
624                Arc::new(Mutex::new(None::<Box<dyn Fn() + Send + Sync>>));
625
626            *create_expiration_timeout.lock().unwrap() = Some(Box::new({
627                let timeout = Arc::clone(&timeout);
628                let elapsed = Arc::clone(&elapsed);
629                let create_expiration_timeout = Arc::clone(&create_expiration_timeout);
630
631                move || {
632                    if let Some(timeout) = timeout.lock().unwrap().take() {
633                        timeout.clear();
634                    }
635
636                    let time_remaining =
637                        delay - elapsed.load(std::sync::atomic::Ordering::Relaxed) as i64;
638                    let timeout_length = time_remaining.min(MAX_TIMEOUT_DELAY);
639
640                    let elapsed = Arc::clone(&elapsed);
641                    let create_expiration_timeout = Arc::clone(&create_expiration_timeout);
642
643                    *timeout.lock().unwrap() = set_timeout_with_handle(
644                        move || {
645                            let elapsed = elapsed.fetch_add(
646                                timeout_length as i32,
647                                std::sync::atomic::Ordering::Relaxed,
648                            ) as i64
649                                + timeout_length;
650
651                            if elapsed < delay {
652                                if let Some(create_expiration_timeout) =
653                                    create_expiration_timeout.lock().unwrap().as_ref()
654                                {
655                                    create_expiration_timeout();
656                                }
657                                return;
658                            }
659
660                            set_cookie.set(None);
661                        },
662                        std::time::Duration::from_millis(timeout_length as u64),
663                    )
664                    .ok();
665                }
666            }));
667
668            if let Some(create_expiration_timeout) =
669                create_expiration_timeout.lock().unwrap().as_ref()
670            {
671                create_expiration_timeout();
672            };
673        }
674
675        #[cfg(feature = "ssr")]
676        {
677            let _ = set_cookie;
678            let _ = delay;
679        }
680    }
681}
682
683#[cfg(not(feature = "ssr"))]
684fn write_client_cookie(
685    name: &str,
686    value: &Option<String>,
687    jar: &mut CookieJar,
688    max_age: Option<i64>,
689    expires: Option<i64>,
690    domain: &Option<String>,
691    path: &Option<String>,
692    same_site: Option<SameSite>,
693    secure: bool,
694    http_only: bool,
695    ssr_cookies_header_getter: Arc<dyn Fn() -> Option<String> + Send + Sync>,
696) {
697    use wasm_bindgen::JsCast;
698
699    update_client_cookie_jar(
700        name,
701        value,
702        jar,
703        max_age,
704        expires,
705        domain,
706        path,
707        same_site,
708        secure,
709        http_only,
710        ssr_cookies_header_getter,
711    );
712
713    let document = document();
714    let document: &web_sys::HtmlDocument = document.unchecked_ref();
715
716    document.set_cookie(&cookie_jar_to_string(jar, name)).ok();
717}
718
719#[cfg(not(feature = "ssr"))]
720fn update_client_cookie_jar(
721    name: &str,
722    value: &Option<String>,
723    jar: &mut CookieJar,
724    max_age: Option<i64>,
725    expires: Option<i64>,
726    domain: &Option<String>,
727    path: &Option<String>,
728    same_site: Option<SameSite>,
729    secure: bool,
730    http_only: bool,
731    ssr_cookies_header_getter: Arc<dyn Fn() -> Option<String> + Send + Sync>,
732) {
733    if let Some(new_jar) = load_and_parse_cookie_jar(ssr_cookies_header_getter) {
734        *jar = new_jar;
735        if let Some(value) = value {
736            let cookie = build_cookie_from_options(
737                name, max_age, expires, http_only, secure, path, same_site, domain, value,
738            );
739
740            jar.add_original(cookie);
741        } else {
742            let max_age = Some(0);
743            let expires = Some(0);
744            let value = "";
745            let cookie = build_cookie_from_options(
746                name, max_age, expires, http_only, secure, path, same_site, domain, value,
747            );
748
749            jar.add(cookie);
750        }
751    }
752}
753
754#[cfg(not(feature = "ssr"))]
755fn cookie_jar_to_string(jar: &CookieJar, name: &str) -> String {
756    match jar.get(name) {
757        Some(c) => c.encoded().to_string(),
758        None => "".to_string(),
759    }
760}
761
762fn build_cookie_from_options(
763    name: &str,
764    max_age: Option<i64>,
765    expires: Option<i64>,
766    http_only: bool,
767    secure: bool,
768    path: &Option<String>,
769    same_site: Option<SameSite>,
770    domain: &Option<String>,
771    value: &str,
772) -> Cookie<'static> {
773    let mut cookie = Cookie::build((name, value));
774    if let Some(max_age) = max_age {
775        cookie = cookie.max_age(Duration::milliseconds(max_age));
776    }
777    if let Some(expires) = expires {
778        match OffsetDateTime::from_unix_timestamp(expires) {
779            Ok(expires) => {
780                cookie = cookie.expires(expires);
781            }
782            Err(err) => {
783                debug_warn!("failed to set cookie expiration: {:?}", err);
784            }
785        }
786    }
787    if http_only {
788        cookie = cookie.http_only(true);
789    }
790    if secure {
791        cookie = cookie.secure(true);
792    }
793    if let Some(domain) = domain {
794        cookie = cookie.domain(domain);
795    }
796    if let Some(path) = path {
797        cookie = cookie.path(path);
798    }
799    if let Some(same_site) = same_site {
800        cookie = cookie.same_site(same_site);
801    }
802
803    let cookie: Cookie = cookie.into();
804    cookie.into_owned()
805}
806
807#[cfg(feature = "ssr")]
808fn write_server_cookie(
809    name: &str,
810    value: Option<String>,
811    jar: &mut CookieJar,
812    max_age: Option<i64>,
813    expires: Option<i64>,
814    domain: Option<String>,
815    path: Option<String>,
816    same_site: Option<SameSite>,
817    secure: bool,
818    http_only: bool,
819    ssr_set_cookie: Arc<dyn Fn(&Cookie) + Send + Sync>,
820) {
821    if let Some(value) = value {
822        let cookie: Cookie = build_cookie_from_options(
823            name, max_age, expires, http_only, secure, &path, same_site, &domain, &value,
824        );
825
826        jar.add(cookie.into_owned());
827    } else {
828        jar.remove(name.to_owned());
829    }
830
831    for cookie in jar.delta() {
832        ssr_set_cookie(cookie);
833    }
834}
835
836fn load_and_parse_cookie_jar(
837    ssr_cookies_header_getter: Arc<dyn Fn() -> Option<String> + Send + Sync>,
838) -> Option<CookieJar> {
839    read_cookies_string(ssr_cookies_header_getter).map(|cookies| {
840        let mut jar = CookieJar::new();
841        for cookie in Cookie::split_parse_encoded(cookies).flatten() {
842            jar.add_original(cookie);
843        }
844
845        jar
846    })
847}