Skip to main content

larust_http/
preferences.rs

1//! Long-lived, unsigned, client-writable preference cookies - backing
2//! `@globals`' `persist` entries (`persist theme = "dark"`, see
3//! `docs/MACROS.md`). Deliberately **not** the session cookie
4//! (`session.rs`): that one is scoped to the current browser session
5//! (`Expiry::OnSessionEnd` by default - no `.with_expiry(...)` call
6//! anywhere in this crate) and rotates on every login
7//! (`Session::cycle_id()`, see `larust_auth::guard::login`) - the wrong
8//! lifetime for a preference that should survive both. Also deliberately
9//! **not** signed/encrypted (`axum_extra`'s `PrivateCookieJar`/
10//! `SignedCookieJar`): tampering with your own theme preference has no
11//! security consequence, so the `Key`-management complexity those add
12//! isn't worth it here.
13//!
14//! Read-only from the server's side, by design - the write path is the
15//! browser setting `document.cookie` directly (the same "no server round
16//! trip for a purely-local UI preference" reasoning already applies to a
17//! plain client-side toggle), not a POST endpoint this module would need
18//! to expose. See `demo/resources/views/layouts/app.blade.xr`'s
19//! theme-toggle script for the real write side.
20
21pub use axum_extra::extract::cookie::CookieJar;
22
23/// Every persisted-preference cookie is named `larust_pref_{name}` - keeps
24/// this whole category of cookie visibly distinct (in devtools, in a
25/// cookie-consent audit) from the session cookie or any future unrelated
26/// app cookie, and impossible to collide with either.
27fn cookie_name(name: &str) -> String {
28    format!("larust_pref_{name}")
29}
30
31/// Reads a `persist` global's current value - `None` when the browser
32/// never set it (a first visit, or the user cleared cookies), in which
33/// case the `@globals` block's own fallback expression is what actually
34/// renders. See `larust_macros`'s `Node::PersistGlobal` codegen arm, the
35/// only caller this function is really designed for.
36pub fn get(cookies: &CookieJar, name: &str) -> Option<String> {
37    cookies
38        .get(&cookie_name(name))
39        .map(|c| c.value().to_string())
40}
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45    use axum_extra::extract::cookie::Cookie;
46
47    #[test]
48    fn returns_none_when_the_cookie_is_absent() {
49        let cookies = CookieJar::new();
50        assert_eq!(get(&cookies, "theme"), None);
51    }
52
53    #[test]
54    fn returns_the_value_of_the_prefixed_cookie() {
55        let cookies = CookieJar::new().add(Cookie::new("larust_pref_theme", "light"));
56        assert_eq!(get(&cookies, "theme"), Some("light".to_string()));
57    }
58
59    #[test]
60    fn an_unrelated_cookie_of_the_same_bare_name_is_not_matched() {
61        // A plain `theme` cookie (no `larust_pref_` prefix) must never be
62        // read as the preference - the prefix is what keeps this category
63        // of cookie unambiguous, not a convention this function trusts the
64        // caller to have followed.
65        let cookies = CookieJar::new().add(Cookie::new("theme", "light"));
66        assert_eq!(get(&cookies, "theme"), None);
67    }
68}