Skip to main content

ocre/
session.rs

1//! Sessions stored in an encrypted cookie, like Rails' default cookie store,
2//! plus flash messages.
3//!
4//! The cookie is encrypted and authenticated with AES-256-GCM using a key
5//! derived from the `SECRET_KEY_BASE` Worker secret, so clients can neither
6//! read nor change it. Nothing is stored on the server: sessions cost no D1
7//! rows and no KV operations. Browsers cap a cookie at 4 KB, so keep ids in
8//! the session, not records.
9
10use std::sync::{Arc, Mutex, MutexGuard};
11
12use axum::{
13    extract::FromRequestParts,
14    http::{HeaderMap, HeaderValue, header, request::Parts},
15};
16use cookie::{Cookie, CookieJar, Key, SameSite};
17use serde::{Serialize, de::DeserializeOwned};
18use serde_json::{Map, Value};
19
20use crate::{Error, Result};
21
22/// Extractor failures render as HTML pages in full-stack apps and as JSON in
23/// API-only apps.
24#[cfg(feature = "html")]
25pub(crate) type Rejection = Error;
26#[cfg(not(feature = "html"))]
27pub(crate) type Rejection = crate::ApiError;
28
29#[cfg(feature = "html")]
30pub(crate) fn reject(err: Error) -> Rejection {
31    err
32}
33
34#[cfg(not(feature = "html"))]
35pub(crate) fn reject(err: Error) -> Rejection {
36    crate::ApiError::from(err)
37}
38
39/// Name of the session cookie: `_ocre_session`.
40///
41/// The cookie is `HttpOnly`, `SameSite=Lax`, `Path=/`, and `Secure` on HTTPS
42/// requests.
43///
44/// # Examples
45///
46/// ```
47/// assert_eq!(ocre::SESSION_COOKIE, "_ocre_session");
48/// ```
49pub const SESSION_COOKIE: &str = "_ocre_session";
50/// Name of the Worker secret the session encryption key is derived from: `SECRET_KEY_BASE`.
51///
52/// It must be at least 64 characters. `ocre secret` generates one, `ocre new`
53/// writes a local one to `.dev.vars`, and `ocre deploy` uploads the production
54/// one. Without it, requests that read an existing session cookie or change
55/// the session fail with [`Error::Internal`] naming this fix. [`jwt`](crate::jwt)
56/// derives its signing key from the same secret.
57///
58/// # Examples
59///
60/// ```no_run
61/// use axum::extract::State;
62/// use ocre::{Ctx, Result, SECRET_KEY_BASE};
63///
64/// async fn secret_is_set(State(ctx): State<Ctx>) -> Result<String> {
65///     Ok(ctx.env().secret(SECRET_KEY_BASE).is_ok().to_string())
66/// }
67/// # let _ = secret_is_set;
68/// ```
69pub const SECRET_KEY_BASE: &str = "SECRET_KEY_BASE";
70/// Browsers drop cookies larger than this (name, value and attributes).
71const MAX_COOKIE_BYTES: usize = 4096;
72/// Session entry holding the flash messages for the next request.
73const FLASH_KEY: &str = "_flash";
74/// Session entry holding the Unix time (seconds) the session expires at.
75const EXPIRES_AT_KEY: &str = "_expires_at";
76/// Session entry set to `true` when the cookie outlives the browser session.
77const PERSISTENT_KEY: &str = "_persistent";
78
79/// Name of the Worker secret listing the previous `SECRET_KEY_BASE` values, during a rotation.
80///
81/// Comma-separated, newest first, each 64 characters or more. Session
82/// cookies encrypted with one of them are still read, then re-encrypted with
83/// the current `SECRET_KEY_BASE` on the same response; JWTs signed with one
84/// still verify until they expire. Rails' `cookies_rotations`. Remove it once
85/// the longest session you care about has been re-encrypted (or after your
86/// JWT lifetime). Worker secrets cannot be read back: keep the value you
87/// replace, and upload it as `SECRET_KEY_BASE_PREVIOUS` with the new
88/// `SECRET_KEY_BASE` (`ocre secrets push SECRET_KEY_BASE_PREVIOUS SECRET_KEY_BASE --file .prod.vars`). Read once per
89/// request; no binding call.
90///
91/// # Examples
92///
93/// ```
94/// assert_eq!(ocre::SECRET_KEY_BASE_PREVIOUS, "SECRET_KEY_BASE_PREVIOUS");
95/// ```
96pub const SECRET_KEY_BASE_PREVIOUS: &str = "SECRET_KEY_BASE_PREVIOUS";
97
98/// The session cookie keys: the current one encrypts, every one decrypts.
99#[derive(Clone, Debug)]
100pub(crate) struct Keys {
101    pub current: Key,
102    pub previous: Vec<Key>,
103}
104
105/// Derives the cookie encryption key from `SECRET_KEY_BASE` (tests).
106#[cfg(test)]
107pub(crate) fn key_from_secret(secret: Option<String>) -> std::result::Result<Keys, String> {
108    keys_from_secrets(secret, None)
109}
110
111/// Derives the current key from `SECRET_KEY_BASE` and the previous ones from
112/// `SECRET_KEY_BASE_PREVIOUS` (comma-separated).
113pub(crate) fn keys_from_secrets(secret: Option<String>, previous: Option<String>) -> std::result::Result<Keys, String> {
114    let current = Key::derive_from(checked_secret(secret)?.as_bytes());
115    let previous = previous_secrets(previous)?.iter().map(|secret| Key::derive_from(secret.as_bytes())).collect();
116    Ok(Keys { current, previous })
117}
118
119/// The values of `SECRET_KEY_BASE_PREVIOUS`, each checked like `SECRET_KEY_BASE`.
120pub(crate) fn previous_secrets(previous: Option<String>) -> std::result::Result<Vec<String>, String> {
121    let previous = previous.unwrap_or_default();
122    let secrets = previous.split(',').map(str::trim).filter(|secret| !secret.is_empty());
123    secrets
124        .map(|secret| {
125            checked_secret(Some(secret.to_owned())).map_err(|_| {
126                format!(
127                    "{SECRET_KEY_BASE_PREVIOUS} has a value shorter than 64 characters. Fix: list old \
128                     SECRET_KEY_BASE values, comma-separated, newest first"
129                )
130            })
131        })
132        .collect()
133}
134
135/// `SECRET_KEY_BASE` when it is set and long enough, or an error naming the fix.
136pub(crate) fn checked_secret(secret: Option<String>) -> std::result::Result<String, String> {
137    let fix = "Fix: run `ocre secret`, put the value in .dev.vars as SECRET_KEY_BASE=... for `ocre dev` \
138               (`ocre new` does this), and deploy with `ocre deploy`, which uploads it";
139    match secret {
140        None => Err(format!("the {SECRET_KEY_BASE} secret is not set. {fix}")),
141        Some(secret) if secret.len() < 64 => Err(format!("{SECRET_KEY_BASE} is shorter than 64 characters. {fix}")),
142        Some(secret) => Ok(secret),
143    }
144}
145
146/// The current request's session, stored in an encrypted cookie, as an extractor.
147///
148/// Like Rails' default cookie store: values are serialized as JSON and the
149/// cookie is encrypted and authenticated with AES-256-GCM using a key derived
150/// from [`SECRET_KEY_BASE`], so clients can neither read nor change it. A
151/// cookie that does not decrypt (tampered, or encrypted with a key that is
152/// neither `SECRET_KEY_BASE` nor listed in [`SECRET_KEY_BASE_PREVIOUS`])
153/// starts an empty session; one encrypted with a previous key is read and
154/// re-encrypted with the current key. The cookie is decrypted on first use;
155/// changes are sent back as one `Set-Cookie` header when the handler returns,
156/// and only if something changed. Clones share the same session.
157///
158/// Browsers drop cookies over 4 KB: store ids, not records. A response whose
159/// session cookie would be larger becomes a 500 (logged, naming that fix).
160///
161/// By default the cookie lasts until the browser session ends and the
162/// session never expires on its own. [`expire_in`](Self::expire_in) sets an
163/// expiry time stored inside the encrypted cookie (checked on every request,
164/// so an old copy of the cookie cannot be replayed after it), and
165/// [`remember_for`](Self::remember_for) also keeps the cookie across browser
166/// restarts ("remember me"). Keys starting with `_` are reserved for Ocre.
167///
168/// Server-side session tracking (list and revoke a user's sessions) is
169/// generated by `ocre g auth --db-sessions`, which stores a session id here
170/// and the session row in D1.
171///
172/// Added by [`serve`](crate::serve). Outside `serve` the extractor rejects
173/// with [`Error::Internal`]: as an HTML page with the `html` feature, as
174/// [`ApiError`](crate::ApiError) JSON without it.
175///
176/// # Free plan
177///
178/// Nothing is stored on the server: sessions cost no D1 rows and no KV
179/// operations, only a little CPU for AES-GCM.
180///
181/// # Examples
182///
183/// ```no_run
184/// use axum::response::Redirect;
185/// use ocre::{Result, Session};
186///
187/// async fn login(session: Session) -> Result<Redirect> {
188///     session.insert("user_id", 42)?;
189///     session.flash("notice", "Signed in.")?;
190///     Ok(Redirect::to("/"))
191/// }
192///
193/// async fn current_user_id(session: Session) -> Result<String> {
194///     let id: Option<i64> = session.get("user_id")?;
195///     Ok(id.map_or("guest".to_owned(), |id| id.to_string()))
196/// }
197/// # let _ = (login, current_user_id);
198/// ```
199#[derive(Clone)]
200pub struct Session(Arc<Mutex<State>>);
201
202struct State {
203    keys: std::result::Result<Keys, String>,
204    /// Encrypted cookie value from the request, decrypted on first use.
205    cookie: Option<String>,
206    loaded: bool,
207    data: Map<String, Value>,
208    /// Flash messages that arrived with this request.
209    flash: Map<String, Value>,
210    changed: bool,
211    secure: bool,
212}
213
214impl State {
215    fn key(&self) -> Result<&Key> {
216        self.keys.as_ref().map(|keys| &keys.current).map_err(|err| Error::internal(err.clone()))
217    }
218
219    /// The session data in `value`, and whether it was encrypted with a previous key.
220    fn decrypt(&self, value: String) -> Result<(Option<Map<String, Value>>, bool)> {
221        let keys = self.keys.as_ref().map_err(|err| Error::internal(err.clone()))?;
222        let mut jar = CookieJar::new();
223        jar.add_original(Cookie::new(SESSION_COOKIE, value));
224        for (index, key) in std::iter::once(&keys.current).chain(&keys.previous).enumerate() {
225            if let Some(cookie) = jar.private(key).get(SESSION_COOKIE) {
226                return Ok((serde_json::from_str(cookie.value()).ok(), index > 0));
227            }
228        }
229        Ok((None, false))
230    }
231}
232
233impl Session {
234    /// Session for a request with these headers. `secure` marks the cookie
235    /// `Secure` (HTTPS requests).
236    pub(crate) fn from_headers(headers: &HeaderMap, keys: std::result::Result<Keys, String>, secure: bool) -> Self {
237        let cookie = headers
238            .get_all(header::COOKIE)
239            .iter()
240            .filter_map(|value| value.to_str().ok())
241            .flat_map(Cookie::split_parse_encoded)
242            .filter_map(std::result::Result::ok)
243            .find(|cookie| cookie.name() == SESSION_COOKIE)
244            .map(|cookie| cookie.value().to_owned());
245        Self(Arc::new(Mutex::new(State {
246            keys,
247            cookie,
248            loaded: false,
249            data: Map::new(),
250            flash: Map::new(),
251            changed: false,
252            secure,
253        })))
254    }
255
256    fn state(&self) -> MutexGuard<'_, State> {
257        // A panic while holding the lock already fails the request.
258        self.0.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
259    }
260
261    /// The state with the cookie decrypted. A cookie that does not decrypt
262    /// (tampered, or encrypted with an unknown key) or that expired starts an
263    /// empty session; one encrypted with a previous key is re-encrypted.
264    fn loaded(&self) -> Result<MutexGuard<'_, State>> {
265        let mut state = self.state();
266        if !state.loaded {
267            if let Some(value) = state.cookie.take() {
268                let (data, rotated) = state.decrypt(value)?;
269                state.data = data.unwrap_or_default();
270                state.changed |= rotated;
271            }
272            let expires_at = state.data.get(EXPIRES_AT_KEY).and_then(Value::as_i64);
273            if expires_at.is_some_and(|expires_at| expires_at <= crate::now()) {
274                // Expired: the response deletes the cookie.
275                state.data.clear();
276                state.changed = true;
277            }
278            if let Some(Value::Object(flash)) = state.data.remove(FLASH_KEY) {
279                state.flash = flash;
280                state.changed = true;
281            }
282            state.loaded = true;
283        }
284        Ok(state)
285    }
286
287    /// The loaded state, marked as changed. Fails without a key, before any change.
288    fn writable(&self) -> Result<MutexGuard<'_, State>> {
289        let mut state = self.loaded()?;
290        state.key()?;
291        state.changed = true;
292        Ok(state)
293    }
294
295    /// Makes the session expire `seconds` from now; the cookie still ends with the browser session.
296    ///
297    /// The expiry time is stored inside the encrypted cookie and checked on
298    /// every request, so a copied or replayed cookie stops working after it
299    /// (Rails' "session expiry" countermeasure): the session then starts
300    /// empty. Calling it again moves the expiry. [`clear`](Self::clear)
301    /// removes it with everything else. Generated sign-in (`ocre g auth`)
302    /// calls it or [`remember_for`](Self::remember_for).
303    ///
304    /// # Errors
305    ///
306    /// [`Error::Internal`] when [`SECRET_KEY_BASE`] is missing or shorter
307    /// than 64 characters.
308    ///
309    /// # Examples
310    ///
311    /// ```no_run
312    /// use ocre::{Result, Session};
313    ///
314    /// async fn sign_in(session: Session) -> Result<()> {
315    ///     session.insert("user_id", 42)?;
316    ///     session.expire_in(24 * 3600) // signed out after a day, even if the tab stays open
317    /// }
318    /// # let _ = sign_in;
319    /// ```
320    pub fn expire_in(&self, seconds: i64) -> Result<()> {
321        self.set_expiry(seconds, false)
322    }
323
324    /// Keeps the session for `seconds`, across browser restarts ("remember me").
325    ///
326    /// Like [`expire_in`](Self::expire_in), plus a persistent cookie
327    /// (`Max-Age` set to the time left), so closing the browser does not sign
328    /// the user out. Every later `Set-Cookie` keeps the same end time.
329    ///
330    /// # Errors
331    ///
332    /// [`Error::Internal`] when [`SECRET_KEY_BASE`] is missing or shorter
333    /// than 64 characters.
334    ///
335    /// # Examples
336    ///
337    /// ```no_run
338    /// use ocre::{Result, Session};
339    ///
340    /// async fn sign_in(session: Session, remember_me: bool) -> Result<()> {
341    ///     session.insert("user_id", 42)?;
342    ///     if remember_me { session.remember_for(30 * 24 * 3600) } else { session.expire_in(24 * 3600) }
343    /// }
344    /// # let _ = sign_in;
345    /// ```
346    pub fn remember_for(&self, seconds: i64) -> Result<()> {
347        self.set_expiry(seconds, true)
348    }
349
350    fn set_expiry(&self, seconds: i64, persistent: bool) -> Result<()> {
351        let mut state = self.writable()?;
352        state.data.insert(EXPIRES_AT_KEY.to_owned(), Value::from(crate::now().saturating_add(seconds)));
353        if persistent {
354            state.data.insert(PERSISTENT_KEY.to_owned(), Value::Bool(true));
355        } else {
356            state.data.remove(PERSISTENT_KEY);
357        }
358        Ok(())
359    }
360
361    /// When the session expires, in Unix seconds, if [`expire_in`](Self::expire_in)
362    /// or [`remember_for`](Self::remember_for) set it.
363    ///
364    /// # Errors
365    ///
366    /// [`Error::Internal`] when the request carries a session cookie but
367    /// [`SECRET_KEY_BASE`] is missing or shorter than 64 characters.
368    ///
369    /// # Examples
370    ///
371    /// ```no_run
372    /// use ocre::{Result, Session};
373    ///
374    /// async fn expires(session: Session) -> Result<String> {
375    ///     Ok(session.expires_at()?.map_or("with the browser".to_owned(), |at| format!("at {at}")))
376    /// }
377    /// # let _ = expires;
378    /// ```
379    pub fn expires_at(&self) -> Result<Option<i64>> {
380        Ok(self.loaded()?.data.get(EXPIRES_AT_KEY).and_then(Value::as_i64))
381    }
382
383    /// Returns the value stored under `key`, or `None` if absent or not deserializable as `T`.
384    ///
385    /// # Errors
386    ///
387    /// [`Error::Internal`] when the request carries a session cookie but
388    /// [`SECRET_KEY_BASE`] is missing or shorter than 64 characters.
389    ///
390    /// # Examples
391    ///
392    /// ```no_run
393    /// use ocre::{Error, Result, Session};
394    ///
395    /// async fn dashboard(session: Session) -> Result<String> {
396    ///     let user_id: i64 = session.get("user_id")?.ok_or(Error::Unauthorized)?;
397    ///     Ok(format!("user {user_id}"))
398    /// }
399    /// # let _ = dashboard;
400    /// ```
401    pub fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>> {
402        Ok(self.loaded()?.data.get(key).and_then(|value| T::deserialize(value).ok()))
403    }
404
405    /// Stores `value` under `key`, replacing any previous value.
406    ///
407    /// # Errors
408    ///
409    /// [`Error::Internal`] when `value` does not serialize to JSON, or when
410    /// [`SECRET_KEY_BASE`] is missing or shorter than 64 characters (nothing is
411    /// changed then).
412    ///
413    /// # Examples
414    ///
415    /// ```no_run
416    /// use ocre::{Result, Session};
417    ///
418    /// async fn set_theme(session: Session) -> Result<()> {
419    ///     session.insert("theme", "dark")
420    /// }
421    /// # let _ = set_theme;
422    /// ```
423    pub fn insert(&self, key: &str, value: impl Serialize) -> Result<()> {
424        self.insert_json(key, serde_json::to_value(value))
425    }
426
427    fn insert_json(&self, key: &str, value: serde_json::Result<Value>) -> Result<()> {
428        let value =
429            value.map_err(|err| Error::internal(format!("session value for `{key}` is not serializable: {err}")))?;
430        self.writable()?.data.insert(key.to_owned(), value);
431        Ok(())
432    }
433
434    /// Removes `key` and returns whether it was there.
435    ///
436    /// # Errors
437    ///
438    /// [`Error::Internal`] when [`SECRET_KEY_BASE`] is missing or shorter than
439    /// 64 characters.
440    ///
441    /// # Examples
442    ///
443    /// ```no_run
444    /// use ocre::{Result, Session};
445    ///
446    /// async fn reset_theme(session: Session) -> Result<String> {
447    ///     Ok(if session.remove("theme")? { "reset" } else { "already default" }.to_owned())
448    /// }
449    /// # let _ = reset_theme;
450    /// ```
451    pub fn remove(&self, key: &str) -> Result<bool> {
452        Ok(self.writable()?.data.remove(key).is_some())
453    }
454
455    /// Empties the session (sign out); flash messages set during this request are kept.
456    ///
457    /// The response then deletes the cookie if nothing is left.
458    ///
459    /// # Errors
460    ///
461    /// [`Error::Internal`] when [`SECRET_KEY_BASE`] is missing or shorter than
462    /// 64 characters.
463    ///
464    /// # Examples
465    ///
466    /// ```no_run
467    /// use axum::response::Redirect;
468    /// use ocre::{Result, Session};
469    ///
470    /// async fn logout(session: Session) -> Result<Redirect> {
471    ///     session.clear()?;
472    ///     session.flash("notice", "Signed out.")?;
473    ///     Ok(Redirect::to("/"))
474    /// }
475    /// # let _ = logout;
476    /// ```
477    pub fn clear(&self) -> Result<()> {
478        self.writable()?.data.retain(|key, _| key == FLASH_KEY);
479        Ok(())
480    }
481
482    /// Stores `message` under `kind` to show on the next request, usually after a redirect.
483    ///
484    /// Kinds are free-form; generated code uses `notice` and `alert`. A second
485    /// message of the same kind replaces the first. The next request reads it
486    /// with [`Flash`] (or [`flashes`](Self::flashes)), after which it is gone.
487    ///
488    /// # Errors
489    ///
490    /// [`Error::Internal`] when [`SECRET_KEY_BASE`] is missing or shorter than
491    /// 64 characters.
492    ///
493    /// # Examples
494    ///
495    /// ```no_run
496    /// use axum::response::Redirect;
497    /// use ocre::{Result, Session};
498    ///
499    /// async fn create(session: Session) -> Result<Redirect> {
500    ///     session.flash("notice", "Post was successfully created.")?;
501    ///     Ok(Redirect::to("/posts"))
502    /// }
503    /// # let _ = create;
504    /// ```
505    pub fn flash(&self, kind: &str, message: impl Into<String>) -> Result<()> {
506        self.flash_message(kind, message.into())
507    }
508
509    fn flash_message(&self, kind: &str, message: String) -> Result<()> {
510        let mut state = self.writable()?;
511        let flash = state.data.entry(FLASH_KEY).or_insert_with(|| Value::Object(Map::new()));
512        if let Value::Object(flash) = flash {
513            flash.insert(kind.to_owned(), Value::String(message));
514        }
515        Ok(())
516    }
517
518    /// Returns the flash messages set by the previous request and removes them from the session.
519    ///
520    /// The [`Flash`] extractor calls it.
521    ///
522    /// # Errors
523    ///
524    /// [`Error::Internal`] when the request carries a session cookie but
525    /// [`SECRET_KEY_BASE`] is missing or shorter than 64 characters.
526    ///
527    /// # Examples
528    ///
529    /// ```no_run
530    /// use ocre::{Result, Session};
531    ///
532    /// async fn index(session: Session) -> Result<String> {
533    ///     Ok(session.flashes()?.notice().unwrap_or_default().to_owned())
534    /// }
535    /// # let _ = index;
536    /// ```
537    pub fn flashes(&self) -> Result<Flash> {
538        let state = self.loaded()?;
539        Ok(Flash(state.flash.iter().filter_map(|(k, v)| Some((k.clone(), v.as_str()?.to_owned()))).collect()))
540    }
541
542    /// Keeps `flash` for the next request too (Rails' `flash.keep`), e.g.
543    /// when a page read the messages but redirects again before showing them.
544    ///
545    /// # Errors
546    ///
547    /// [`Error::Internal`] when [`SECRET_KEY_BASE`] is missing or shorter
548    /// than 64 characters.
549    ///
550    /// # Examples
551    ///
552    /// ```no_run
553    /// use axum::response::Redirect;
554    /// use ocre::{Flash, Result, Session};
555    ///
556    /// async fn old_dashboard(session: Session, flash: Flash) -> Result<Redirect> {
557    ///     session.keep_flash(&flash)?;
558    ///     Ok(Redirect::to("/dashboard"))
559    /// }
560    /// # let _ = old_dashboard;
561    /// ```
562    pub fn keep_flash(&self, flash: &Flash) -> Result<()> {
563        for (kind, message) in flash.iter() {
564            self.flash_message(kind, message.to_owned())?;
565        }
566        Ok(())
567    }
568
569    /// `Set-Cookie` value when the session changed during the request.
570    pub(crate) fn set_cookie(&self) -> Result<Option<HeaderValue>> {
571        let state = self.state();
572        if !state.changed {
573            return Ok(None);
574        }
575        let (key, data) = (state.key()?, &state.data);
576        let mut cookie = if data.is_empty() {
577            // Not `make_removal`: it reads the clock, which panics on
578            // wasm32-unknown-unknown. A past date and Max-Age=0 do the same.
579            let mut cookie = Cookie::from(SESSION_COOKIE);
580            cookie.set_max_age(cookie::time::Duration::ZERO);
581            cookie.set_expires(cookie::time::OffsetDateTime::UNIX_EPOCH);
582            cookie
583        } else {
584            let json = serde_json::to_string(data).expect("JSON values serialize");
585            let mut jar = CookieJar::new();
586            jar.private_mut(key).add(Cookie::new(SESSION_COOKIE, json));
587            let mut cookie = jar.get(SESSION_COOKIE).expect("just added").clone();
588            if data.get(PERSISTENT_KEY) == Some(&Value::Bool(true)) {
589                let expires_at = data.get(EXPIRES_AT_KEY).and_then(Value::as_i64).unwrap_or(0);
590                cookie.set_max_age(cookie::time::Duration::seconds(expires_at.saturating_sub(crate::now()).max(0)));
591            }
592            cookie
593        };
594        cookie.set_path("/");
595        cookie.set_http_only(true);
596        cookie.set_same_site(SameSite::Lax);
597        cookie.set_secure(state.secure);
598        let header = cookie.encoded().to_string();
599        if header.len() > MAX_COOKIE_BYTES {
600            return Err(Error::internal(format!(
601                "the session cookie would be {} bytes; browsers drop cookies over {MAX_COOKIE_BYTES}. Fix: store ids in the session, not records",
602                header.len()
603            )));
604        }
605        Ok(Some(HeaderValue::from_str(&header).expect("encoded cookies are valid header values")))
606    }
607}
608
609impl<S: Sync> FromRequestParts<S> for Session {
610    type Rejection = Rejection;
611
612    async fn from_request_parts(parts: &mut Parts, _state: &S) -> std::result::Result<Self, Rejection> {
613        parts.extensions.get::<Session>().cloned().ok_or_else(|| {
614            reject(Error::internal("no session on this request. Fix: serve the app with `ocre::serve`, which adds it"))
615        })
616    }
617}
618
619/// Flash messages set by the previous request, as an extractor.
620///
621/// Extracting them removes them from the session, so each message shows once
622/// (Rails' `flash`). Rejects like [`Session`] when used outside
623/// [`serve`](crate::serve) or when the secret is missing.
624///
625/// In templates: `{% if let Some(notice) = flash.notice() %}<p>{{ notice }}</p>{% endif %}`.
626///
627/// # Examples
628///
629/// ```no_run
630/// use ocre::Flash;
631///
632/// async fn index(flash: Flash) -> String {
633///     flash.iter().map(|(kind, message)| format!("{kind}: {message}\n")).collect()
634/// }
635/// # let _ = index;
636/// ```
637#[derive(Debug, Clone, Default, PartialEq, Eq)]
638pub struct Flash(Vec<(String, String)>);
639
640impl Flash {
641    /// The message of the given `kind`, if the previous request set one.
642    ///
643    /// # Examples
644    ///
645    /// ```
646    /// assert_eq!(ocre::Flash::default().get("warning"), None);
647    /// ```
648    pub fn get(&self, kind: &str) -> Option<&str> {
649        self.0.iter().find(|(k, _)| k == kind).map(|(_, message)| message.as_str())
650    }
651
652    /// The `notice` message (success), same as `flash.get("notice")`.
653    ///
654    /// # Examples
655    ///
656    /// ```
657    /// assert_eq!(ocre::Flash::default().notice(), None);
658    /// ```
659    pub fn notice(&self) -> Option<&str> {
660        self.get("notice")
661    }
662
663    /// The `alert` message (failure), same as `flash.get("alert")`.
664    ///
665    /// # Examples
666    ///
667    /// ```
668    /// assert_eq!(ocre::Flash::default().alert(), None);
669    /// ```
670    pub fn alert(&self) -> Option<&str> {
671        self.get("alert")
672    }
673
674    /// Every `(kind, message)` pair.
675    ///
676    /// # Examples
677    ///
678    /// ```
679    /// assert_eq!(ocre::Flash::default().iter().count(), 0);
680    /// ```
681    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
682        self.0.iter().map(|(kind, message)| (kind.as_str(), message.as_str()))
683    }
684
685    /// Whether there is no message.
686    ///
687    /// # Examples
688    ///
689    /// ```
690    /// assert!(ocre::Flash::default().is_empty());
691    /// ```
692    pub fn is_empty(&self) -> bool {
693        self.0.is_empty()
694    }
695
696    /// These messages plus `message` of `kind`, for this response only
697    /// (Rails' `flash.now`): render a page with a message without
698    /// storing it in the session, e.g. a form shown again with an alert.
699    ///
700    /// # Examples
701    ///
702    /// ```
703    /// let flash = ocre::Flash::default().now("alert", "Check the highlighted fields.");
704    /// assert_eq!(flash.alert(), Some("Check the highlighted fields."));
705    /// assert_eq!(flash.clone().now("alert", "Again.").alert(), Some("Again."), "a kind has one message");
706    /// ```
707    #[must_use]
708    pub fn now(self, kind: &str, message: impl Into<String>) -> Self {
709        self.with_message(kind, message.into())
710    }
711
712    fn with_message(mut self, kind: &str, message: String) -> Self {
713        match self.0.iter_mut().find(|(existing, _)| existing == kind) {
714            Some(entry) => entry.1 = message,
715            None => self.0.push((kind.to_owned(), message)),
716        }
717        self
718    }
719}
720
721impl<S: Sync> FromRequestParts<S> for Flash {
722    type Rejection = Rejection;
723
724    async fn from_request_parts(parts: &mut Parts, state: &S) -> std::result::Result<Self, Rejection> {
725        Session::from_request_parts(parts, state).await?.flashes().map_err(reject)
726    }
727}
728
729#[cfg(test)]
730#[path = "../tests/session.rs"]
731mod tests;