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