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};
6pub use cookie::SameSite;
7use cookie::time::{Duration, OffsetDateTime};
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 + PartialEq + 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 + PartialEq + 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 UseBroadcastChannelReturn, WatchPausableReturn, use_broadcast_channel, watch_pausable,
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 let prev_value = jar
257 .read_value()
258 .get(&cookie_name)
259 .map(|c| c.value().to_string());
260
261 if prev_value == value {
262 return;
263 }
264
265 if let Some(prev_value) = prev_value
266 && let Ok(prev_parsed_value) = C::decode(&prev_value)
267 && let Some(cookie) = cookie.try_read_untracked()
268 && Some(prev_parsed_value) == *cookie
269 {
270 return;
271 }
272
273 jar.update_value(|jar| {
274 write_client_cookie(
275 &cookie_name,
276 &value,
277 jar,
278 max_age,
279 expires,
280 &domain,
281 &path,
282 same_site,
283 secure,
284 http_only,
285 Arc::clone(&ssr_cookies_header_getter),
286 );
287 });
288
289 post(&value);
290 }
291 }
292 };
293
294 let WatchPausableReturn {
295 pause,
296 resume,
297 stop,
298 ..
299 } = watch_pausable(move || cookie.track(), {
300 let on_cookie_change = on_cookie_change.clone();
301
302 move |_, _, _| {
303 on_cookie_change();
304 }
305 });
306
307 // listen to cookie changes from the broadcast channel
308 Effect::new({
309 let ssr_cookies_header_getter = Arc::clone(&ssr_cookies_header_getter);
310 let cookie_name = cookie_name.to_owned();
311
312 move |_| {
313 if let Some(message) = message.get() {
314 pause();
315
316 if let Some(message) = message {
317 match C::decode(&message) {
318 Ok(value) => {
319 let ssr_cookies_header_getter =
320 Arc::clone(&ssr_cookies_header_getter);
321
322 jar.update_value(|jar| {
323 update_client_cookie_jar(
324 &cookie_name,
325 &Some(message),
326 jar,
327 max_age,
328 expires,
329 &domain,
330 &path,
331 same_site,
332 secure,
333 http_only,
334 ssr_cookies_header_getter,
335 );
336 });
337
338 set_cookie.set(Some(value));
339 }
340 Err(err) => {
341 on_error(CodecError::Decode(err));
342 }
343 }
344 } else {
345 let cookie_name = cookie_name.clone();
346 let ssr_cookies_header_getter = Arc::clone(&ssr_cookies_header_getter);
347
348 jar.update_value(|jar| {
349 update_client_cookie_jar(
350 &cookie_name,
351 &None,
352 jar,
353 max_age,
354 expires,
355 &domain,
356 &path,
357 same_site,
358 secure,
359 http_only,
360 ssr_cookies_header_getter,
361 );
362 jar.force_remove(cookie_name);
363 });
364
365 set_cookie.set(None);
366 }
367
368 resume();
369 }
370 }
371 });
372
373 on_cleanup(move || {
374 stop();
375 on_cookie_change();
376 });
377
378 let _ = ssr_set_cookie;
379 }
380
381 #[cfg(feature = "ssr")]
382 {
383 if !readonly {
384 let _ = ImmediateEffect::new_isomorphic({
385 let cookie_name = cookie_name.to_owned();
386 let ssr_set_cookie = Arc::clone(&ssr_set_cookie);
387
388 let lock = Arc::new(std::sync::Mutex::new(()));
389
390 move || {
391 if let Ok(_lock_guard) = lock.clone().lock() {
392 let domain = domain.clone();
393 let path = path.clone();
394
395 if let Some(value) = cookie.try_with(|cookie| {
396 cookie.as_ref().map(|cookie| {
397 C::encode(cookie)
398 .map_err(|err| on_error(CodecError::Encode(err)))
399 .ok()
400 })
401 }) {
402 jar.update_value({
403 let domain = domain.clone();
404 let path = path.clone();
405 let ssr_set_cookie = Arc::clone(&ssr_set_cookie);
406
407 |jar| {
408 write_server_cookie(
409 &cookie_name,
410 value.flatten(),
411 jar,
412 max_age,
413 expires,
414 domain,
415 path,
416 same_site,
417 secure,
418 http_only,
419 ssr_set_cookie,
420 )
421 }
422 });
423 }
424 }
425 }
426 });
427 }
428 }
429
430 (cookie.into(), set_cookie)
431}
432
433/// Options for [`use_cookie_with_options`].
434#[derive(DefaultBuilder)]
435pub struct UseCookieOptions<T, E, D> {
436 /// [`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.
437 /// Default: `None`
438 ///
439 /// > The [cookie storage model specification](https://tools.ietf.org/html/rfc6265#section-5.3) states
440 /// > that if both `expires` and `max_age` is set, then `max_age` takes precedence,
441 /// > but not all clients may obey this, so if both are set, they should point to the same date and time!
442 ///
443 /// > If neither of `expires` and `max_age` is set, the cookie will be session-only and removed when the user closes their browser.
444 #[builder(into)]
445 max_age: Option<i64>,
446
447 /// [Expiration date-time of the cookie](https://tools.ietf.org/html/rfc6265#section-5.2.1) as UNIX timestamp in seconds.
448 /// The signal will turn to `None` after the expiration date-time is reached.
449 /// Default: `None`
450 ///
451 /// > The [cookie storage model specification](https://tools.ietf.org/html/rfc6265#section-5.3) states
452 /// > that if both `expires` and `max_age` is set, then `max_age` takes precedence,
453 /// > but not all clients may obey this, so if both are set, they should point to the same date and time!
454 ///
455 /// > If neither of `expires` and `max_age` is set, the cookie will be session-only and removed when the user closes their browser.
456 #[builder(into)]
457 expires: Option<i64>,
458
459 /// Specifies the [`HttpOnly` cookie attribute](https://tools.ietf.org/html/rfc6265#section-5.2.6).
460 /// When `true`, the `HttpOnly` attribute is set; otherwise it is not.
461 /// By default, the `HttpOnly` attribute is not set.
462 ///
463 /// > Be careful when setting this to `true`, as compliant clients will not allow client-side JavaScript to see the cookie in `document.cookie`.
464 http_only: bool,
465
466 /// Specifies the value for the [`Secure` cookie attribute](https://tools.ietf.org/html/rfc6265#section-5.2.5).
467 /// When `true`, the `Secure` attribute is set; otherwise it is not.
468 /// By default, the `Secure` attribute is not set.
469 ///
470 /// > Be careful when setting this to `true`, as compliant clients will not send the cookie back to the
471 /// > server in the future if the browser does not have an HTTPS connection. This can lead to hydration errors.
472 secure: bool,
473
474 /// Specifies the value for the [`Domain` cookie attribute](https://tools.ietf.org/html/rfc6265#section-5.2.3).
475 /// By default, no domain is set, and most clients will consider applying the cookie only to the current domain.
476 #[builder(into)]
477 domain: Option<String>,
478
479 /// Specifies the value for the [`Path` cookie attribute](https://tools.ietf.org/html/rfc6265#section-5.2.4).
480 /// By default, the path is considered the ["default path"](https://tools.ietf.org/html/rfc6265#section-5.1.4).
481 #[builder(into)]
482 path: Option<String>,
483
484 /// Specifies the value for the [`SameSite` cookie attribute](https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7).
485 ///
486 /// - `'Some(SameSite::Lax)'` will set the `SameSite` attribute to `Lax` for lax same-site enforcement.
487 /// - `'Some(SameSite::None)'` will set the `SameSite` attribute to `None` for an explicit cross-site cookie.
488 /// - `'Some(SameSite::Strict)'` will set the `SameSite` attribute to `Strict` for strict same-site enforcement.
489 /// - `None` will not set the `SameSite` attribute (default).
490 ///
491 /// 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).
492 #[builder(into)]
493 same_site: Option<SameSite>,
494
495 /// The default cookie value in case the cookie is not set.
496 /// Defaults to `None`.
497 default_value: Option<T>,
498
499 /// If `true` the returned `WriteSignal` will not affect the actual cookie.
500 /// Default: `false`
501 readonly: bool,
502
503 /// Getter function to return the string value of the cookie header.
504 /// When you use one of the features `"axum"` or `"actix"` there's a valid default implementation provided.
505 ssr_cookies_header_getter: Arc<dyn Fn() -> Option<String> + Send + Sync>,
506
507 /// Function to add a set cookie header to the response on the server.
508 /// When you use one of the features `"axum"` or `"actix"` there's a valid default implementation provided.
509 ssr_set_cookie: Arc<dyn Fn(&Cookie) + Send + Sync>,
510
511 /// Callback for encoding/decoding errors. Defaults to logging the error to the console.
512 on_error: Arc<dyn Fn(CodecError<E, D>) + Send + Sync>,
513}
514
515impl<T, E, D> Default for UseCookieOptions<T, E, D> {
516 #[allow(dead_code)]
517 fn default() -> Self {
518 Self {
519 max_age: None,
520 expires: None,
521 http_only: false,
522 default_value: None,
523 readonly: false,
524 secure: false,
525 domain: None,
526 path: None,
527 same_site: None,
528 ssr_cookies_header_getter: Arc::new(move || {
529 get_header!(COOKIE, use_cookie, ssr_cookies_header_getter)
530 }),
531 ssr_set_cookie: Arc::new(|cookie: &Cookie| {
532 #[cfg(feature = "ssr")]
533 {
534 server_set_cookie(cookie);
535 }
536
537 let _ = cookie;
538 }),
539 on_error: Arc::new(|_| {
540 error!("cookie (de-/)serialization error");
541 }),
542 }
543 }
544}
545
546fn read_cookies_string(
547 ssr_cookies_header_getter: Arc<dyn Fn() -> Option<String> + Send + Sync>,
548) -> Option<String> {
549 let cookies;
550
551 #[cfg(feature = "ssr")]
552 {
553 cookies = ssr_cookies_header_getter();
554 }
555
556 #[cfg(not(feature = "ssr"))]
557 {
558 use wasm_bindgen::JsCast;
559
560 let _ = ssr_cookies_header_getter;
561
562 let js_value: wasm_bindgen::JsValue = document().into();
563 let document: web_sys::HtmlDocument = js_value.unchecked_into();
564 cookies = Some(document.cookie().unwrap_or_default());
565 }
566
567 cookies
568}
569
570fn handle_expiration<T>(delay: Option<i64>, set_cookie: WriteSignal<Option<T>>)
571where
572 T: Send + Sync + 'static,
573{
574 if let Some(delay) = delay {
575 #[cfg(not(feature = "ssr"))]
576 {
577 use leptos::leptos_dom::helpers::TimeoutHandle;
578 use std::sync::{Mutex, atomic::AtomicI32};
579
580 // The maximum value allowed on a timeout delay.
581 // Reference: https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#maximum_delay_value
582 const MAX_TIMEOUT_DELAY: i64 = 2_147_483_647;
583
584 let timeout = Arc::new(Mutex::new(None::<TimeoutHandle>));
585 let elapsed = Arc::new(AtomicI32::new(0));
586
587 on_cleanup({
588 let timeout = Arc::clone(&timeout);
589 move || {
590 if let Some(timeout) = timeout.lock().unwrap().take() {
591 timeout.clear();
592 }
593 }
594 });
595
596 let create_expiration_timeout =
597 Arc::new(Mutex::new(None::<Box<dyn Fn() + Send + Sync>>));
598
599 *create_expiration_timeout.lock().unwrap() = Some(Box::new({
600 let timeout = Arc::clone(&timeout);
601 let elapsed = Arc::clone(&elapsed);
602 let create_expiration_timeout = Arc::clone(&create_expiration_timeout);
603
604 move || {
605 if let Some(timeout) = timeout.lock().unwrap().take() {
606 timeout.clear();
607 }
608
609 let time_remaining =
610 delay - elapsed.load(std::sync::atomic::Ordering::Relaxed) as i64;
611 let timeout_length = time_remaining.min(MAX_TIMEOUT_DELAY);
612
613 let elapsed = Arc::clone(&elapsed);
614 let create_expiration_timeout = Arc::clone(&create_expiration_timeout);
615
616 *timeout.lock().unwrap() = set_timeout_with_handle(
617 move || {
618 let elapsed = elapsed.fetch_add(
619 timeout_length as i32,
620 std::sync::atomic::Ordering::Relaxed,
621 ) as i64
622 + timeout_length;
623
624 if elapsed < delay {
625 if let Some(create_expiration_timeout) =
626 create_expiration_timeout.lock().unwrap().as_ref()
627 {
628 create_expiration_timeout();
629 }
630 return;
631 }
632
633 set_cookie.set(None);
634 },
635 std::time::Duration::from_millis(timeout_length as u64),
636 )
637 .ok();
638 }
639 }));
640
641 if let Some(create_expiration_timeout) =
642 create_expiration_timeout.lock().unwrap().as_ref()
643 {
644 create_expiration_timeout();
645 };
646 }
647
648 #[cfg(feature = "ssr")]
649 {
650 let _ = set_cookie;
651 let _ = delay;
652 }
653 }
654}
655
656#[cfg(not(feature = "ssr"))]
657fn write_client_cookie(
658 name: &str,
659 value: &Option<String>,
660 jar: &mut CookieJar,
661 max_age: Option<i64>,
662 expires: Option<i64>,
663 domain: &Option<String>,
664 path: &Option<String>,
665 same_site: Option<SameSite>,
666 secure: bool,
667 http_only: bool,
668 ssr_cookies_header_getter: Arc<dyn Fn() -> Option<String> + Send + Sync>,
669) {
670 use wasm_bindgen::JsCast;
671
672 update_client_cookie_jar(
673 name,
674 value,
675 jar,
676 max_age,
677 expires,
678 domain,
679 path,
680 same_site,
681 secure,
682 http_only,
683 ssr_cookies_header_getter,
684 );
685
686 let document = document();
687 let document: &web_sys::HtmlDocument = document.unchecked_ref();
688
689 document.set_cookie(&cookie_jar_to_string(jar, name)).ok();
690}
691
692#[cfg(not(feature = "ssr"))]
693fn update_client_cookie_jar(
694 name: &str,
695 value: &Option<String>,
696 jar: &mut CookieJar,
697 max_age: Option<i64>,
698 expires: Option<i64>,
699 domain: &Option<String>,
700 path: &Option<String>,
701 same_site: Option<SameSite>,
702 secure: bool,
703 http_only: bool,
704 ssr_cookies_header_getter: Arc<dyn Fn() -> Option<String> + Send + Sync>,
705) {
706 if let Some(new_jar) = load_and_parse_cookie_jar(ssr_cookies_header_getter) {
707 *jar = new_jar;
708 if let Some(value) = value {
709 let cookie = build_cookie_from_options(
710 name, max_age, expires, http_only, secure, path, same_site, domain, value,
711 );
712
713 jar.add_original(cookie);
714 } else {
715 let max_age = Some(0);
716 let expires = Some(0);
717 let value = "";
718 let cookie = build_cookie_from_options(
719 name, max_age, expires, http_only, secure, path, same_site, domain, value,
720 );
721
722 jar.add(cookie);
723 }
724 }
725}
726
727#[cfg(not(feature = "ssr"))]
728fn cookie_jar_to_string(jar: &CookieJar, name: &str) -> String {
729 match jar.get(name) {
730 Some(c) => c.encoded().to_string(),
731 None => "".to_string(),
732 }
733}
734
735fn build_cookie_from_options(
736 name: &str,
737 max_age: Option<i64>,
738 expires: Option<i64>,
739 http_only: bool,
740 secure: bool,
741 path: &Option<String>,
742 same_site: Option<SameSite>,
743 domain: &Option<String>,
744 value: &str,
745) -> Cookie<'static> {
746 let mut cookie = Cookie::build((name, value));
747 if let Some(max_age) = max_age {
748 cookie = cookie.max_age(Duration::milliseconds(max_age));
749 }
750 if let Some(expires) = expires {
751 match OffsetDateTime::from_unix_timestamp(expires) {
752 Ok(expires) => {
753 cookie = cookie.expires(expires);
754 }
755 Err(err) => {
756 debug_warn!("failed to set cookie expiration: {:?}", err);
757 }
758 }
759 }
760 if http_only {
761 cookie = cookie.http_only(true);
762 }
763 if secure {
764 cookie = cookie.secure(true);
765 }
766 if let Some(domain) = domain {
767 cookie = cookie.domain(domain);
768 }
769 if let Some(path) = path {
770 cookie = cookie.path(path);
771 }
772 if let Some(same_site) = same_site {
773 cookie = cookie.same_site(same_site);
774 }
775
776 let cookie: Cookie = cookie.into();
777 cookie.into_owned()
778}
779
780#[cfg(feature = "ssr")]
781fn write_server_cookie(
782 name: &str,
783 value: Option<String>,
784 jar: &mut CookieJar,
785 max_age: Option<i64>,
786 expires: Option<i64>,
787 domain: Option<String>,
788 path: Option<String>,
789 same_site: Option<SameSite>,
790 secure: bool,
791 http_only: bool,
792 ssr_set_cookie: Arc<dyn Fn(&Cookie) + Send + Sync>,
793) {
794 if let Some(value) = value {
795 let cookie: Cookie = build_cookie_from_options(
796 name, max_age, expires, http_only, secure, &path, same_site, &domain, &value,
797 );
798
799 jar.add(cookie.into_owned());
800 } else {
801 jar.remove(name.to_owned());
802 }
803
804 for cookie in jar.delta() {
805 ssr_set_cookie(cookie);
806 }
807}
808
809fn load_and_parse_cookie_jar(
810 ssr_cookies_header_getter: Arc<dyn Fn() -> Option<String> + Send + Sync>,
811) -> Option<CookieJar> {
812 read_cookies_string(ssr_cookies_header_getter).map(|cookies| {
813 let mut jar = CookieJar::new();
814 for cookie in Cookie::split_parse_encoded(cookies).flatten() {
815 jar.add_original(cookie);
816 }
817
818 jar
819 })
820}
821
822#[cfg(feature = "ssr")]
823fn server_set_cookie(cookie: &Cookie) {
824 #[cfg(feature = "actix")]
825 use leptos_actix::ResponseOptions;
826 #[cfg(feature = "axum")]
827 use leptos_axum::ResponseOptions;
828
829 #[cfg(feature = "actix")]
830 const SET_COOKIE: http0_2::HeaderName = http0_2::header::SET_COOKIE;
831 #[cfg(feature = "axum")]
832 const SET_COOKIE: http1::HeaderName = http1::header::SET_COOKIE;
833
834 #[cfg(feature = "actix")]
835 type HeaderValue = http0_2::HeaderValue;
836 #[cfg(feature = "axum")]
837 type HeaderValue = http1::HeaderValue;
838
839 #[cfg(all(not(feature = "axum"), not(feature = "actix")))]
840 {
841 use leptos::logging::warn;
842 let _ = cookie;
843 warn!(
844 "If you're using use_cookie without the feature `axum` or `actix` enabled, you should provide the option `ssr_set_cookie`"
845 );
846 }
847
848 #[cfg(any(feature = "axum", feature = "actix"))]
849 {
850 if let Some(response_options) = use_context::<ResponseOptions>() {
851 if let Ok(header_value) = HeaderValue::from_str(&cookie.encoded().to_string()) {
852 response_options.append_header(SET_COOKIE, header_value);
853 }
854 }
855 }
856}