Skip to main content

renox_core/
state.rs

1use std::fmt::Display;
2use std::sync::Arc;
3
4use cookie::Key;
5
6use crate::auth::{Gates, LoginThrottle};
7use crate::cache::Cache;
8use crate::db::Db;
9use crate::events::Listeners;
10use crate::i18n::Translator;
11use crate::mail::Mailer;
12use crate::queue::Queue;
13use crate::storage::Storage;
14use crate::{Config, Result, RouteTable, Views};
15
16/// Shared state available to every handler through `State<AppState>`.
17///
18/// One `Arc` around [`AppStateInner`]: every middleware layer, the request's
19/// extensions and each handler's `State` clone it, so a clone is one atomic
20/// increment (it was a struct of about 35 fields, cloned tens of times per
21/// request: #334). Read its parts as fields, through `Deref`:
22/// `state.db`, `state.config`, `state.queue`…
23#[derive(Clone)]
24pub struct AppState(Arc<AppStateInner>);
25
26impl std::ops::Deref for AppState {
27    type Target = AppStateInner;
28
29    fn deref(&self) -> &AppStateInner {
30        &self.0
31    }
32}
33
34impl AppState {
35    /// Wraps the parts made at boot (`App::boot`).
36    pub(crate) fn new(inner: AppStateInner) -> Self {
37        AppState(Arc::new(inner))
38    }
39}
40
41/// What an [`AppState`] holds. Apps read these as the state's fields
42/// (`state.db`); only Renox builds one, at boot.
43#[non_exhaustive]
44pub struct AppStateInner {
45    /// The configuration.
46    pub config: Arc<Config>,
47    /// Named routes, to build URLs (`state.url(name, params)`).
48    pub(crate) routes: Arc<RouteTable>,
49    /// The template engine (`view()` renders, `mail_view` for mail).
50    pub(crate) views: Views,
51    /// The database connection pool.
52    pub db: Db,
53    /// Sends mail (`MAIL_MAILER`).
54    pub mailer: Mailer,
55    /// The app's other mailers (`App::mailer`), by name.
56    pub(crate) mailers: Arc<std::collections::HashMap<String, Mailer>>,
57    /// Dispatches jobs.
58    pub queue: Queue,
59    /// The cache (`CACHE_STORE`).
60    pub cache: Cache,
61    /// The file storage disk (`STORAGE_DISK`).
62    pub storage: Storage,
63    /// The app's other disks (`App::disk`), by name; see [`AppState::disk`].
64    pub(crate) disks: Arc<std::collections::HashMap<String, Storage>>,
65    /// Calls other services; faked in tests (`TestApp::fake_http`).
66    pub http: crate::http::Http,
67    /// Translations from `LANG_PATH` and the built-in ones (`Lang`, `t()`).
68    pub(crate) translator: Arc<Translator>,
69    /// Live reload, only while developing locally.
70    pub(crate) live: Option<Arc<crate::live::Live>>,
71    /// Wakes the users' open notification streams.
72    pub(crate) notification_hub: Arc<crate::auth::notifications::Hub>,
73    pub(crate) listeners: Listeners,
74    pub(crate) key: Key,
75    /// With `SESSION_DRIVER=database` in tests, sessions are kept here
76    /// instead of the table (keyed by the id's hash).
77    pub(crate) session_mirror:
78        Option<Arc<std::sync::Mutex<std::collections::HashMap<String, String>>>>,
79    pub(crate) gates: Gates,
80    pub(crate) async_gates: Arc<std::collections::HashMap<String, crate::auth::AsyncGate>>,
81    pub(crate) throttle: Arc<LoginThrottle>,
82    /// A module's second login step (`Registry::second_factor`).
83    pub(crate) second_factor: Option<Arc<crate::auth::second_factor::SecondFactor>>,
84    /// Sections other modules add to `/account` (`Registry::account_section`).
85    pub(crate) account_sections: Arc<Vec<crate::auth::account::AccountSection>>,
86    /// The `Auth` module's settings, when the app has it.
87    pub(crate) auth: Option<Arc<crate::auth::module::Settings>>,
88    /// `App::detect_locale`: the browser's `Accept-Language` picks the locale.
89    pub(crate) detect_locale: bool,
90    pub(crate) security: Arc<crate::security::Security>,
91    pub(crate) webhooks: crate::webhook::Handlers,
92    /// Read by the live component route (#433).
93    #[allow(dead_code)]
94    pub(crate) live_components: crate::live_component::LiveMap,
95    /// Values every view gets (`App::share`).
96    pub(crate) shares: Arc<Vec<(String, crate::view::ShareFn)>>,
97    /// The app's notification channels (`App::channel`).
98    pub(crate) channels:
99        Arc<std::collections::HashMap<String, crate::auth::notifications::ChannelFn>>,
100    /// The app's own values (`App::provide`).
101    pub(crate) provided: crate::provided::ProvidedMap,
102    /// `/_renox/debug`'s recent requests, while developing locally.
103    pub(crate) inspector: Option<Arc<crate::inspector::Inspector>>,
104    /// Named rate limiters (`App::rate_limiter`).
105    pub(crate) limiters: Arc<std::collections::HashMap<String, crate::rate_limit::NamedLimiter>>,
106    /// The app's error reporters (`App::report`).
107    pub(crate) reporters: Arc<Vec<crate::report::ReportFn>>,
108    /// What tests asked to record instead of doing (`TestApp::fake_events`, …).
109    pub(crate) fakes: Arc<Fakes>,
110}
111
112/// A notification a test recorded instead of sending.
113#[derive(Debug, Clone)]
114#[non_exhaustive]
115pub struct SentNotification {
116    /// `Notification::kind`.
117    pub kind: &'static str,
118    /// Who it was sent to.
119    pub to: crate::auth::Recipient,
120}
121
122/// An event a test recorded instead of sending it to the open pages
123/// (`TestApp::fake_broadcasts`, `AppState::broadcast`).
124#[derive(Debug, Clone, PartialEq)]
125#[non_exhaustive]
126pub struct SentBroadcast {
127    /// The user whose pages it was for; `None` for every page.
128    pub user_id: Option<i64>,
129    /// The DOM event's name.
130    pub event: String,
131    /// Its data (the DOM event's `detail`).
132    pub data: serde_json::Value,
133}
134
135/// Recorders for `TestApp::fake_events`, `fake_notifications` and
136/// `fake_broadcasts`.
137#[derive(Default)]
138pub(crate) struct Fakes {
139    pub events: std::sync::Mutex<Option<Vec<Box<dyn std::any::Any + Send>>>>,
140    pub notifications: std::sync::Mutex<Option<Vec<SentNotification>>>,
141    pub broadcasts: std::sync::Mutex<Option<Vec<SentBroadcast>>>,
142}
143
144impl Fakes {
145    /// Records `event` if events are faked; returns whether it did.
146    pub(crate) fn record_event<E: Send + 'static>(&self, event: E) -> bool {
147        let mut events = self.events.lock().unwrap_or_else(|e| e.into_inner());
148        match events.as_mut() {
149            Some(list) => {
150                list.push(Box::new(event));
151                true
152            }
153            None => false,
154        }
155    }
156
157    /// Records a broadcast if broadcasts are faked; returns whether it did.
158    pub(crate) fn record_broadcast(&self, broadcast: SentBroadcast) -> bool {
159        let mut sent = self.broadcasts.lock().unwrap_or_else(|e| e.into_inner());
160        match sent.as_mut() {
161            Some(list) => {
162                list.push(broadcast);
163                true
164            }
165            None => false,
166        }
167    }
168
169    /// Records a notification if notifications are faked.
170    pub(crate) fn record_notification(
171        &self,
172        kind: &'static str,
173        to: &crate::auth::Recipient,
174    ) -> bool {
175        let mut sent = self.notifications.lock().unwrap_or_else(|e| e.into_inner());
176        match sent.as_mut() {
177            Some(list) => {
178                list.push(SentNotification {
179                    kind,
180                    to: to.clone(),
181                });
182                true
183            }
184            None => false,
185        }
186    }
187}
188
189impl AppState {
190    /// The URL path of a named route, e.g. `state.url("products.show", &[&id])`.
191    pub fn url(&self, name: &str, params: &[&dyn Display]) -> Result<String> {
192        Ok(self.routes.url(name, params)?)
193    }
194
195    /// Encrypts `plain` with `APP_KEY` (AES-256-GCM), e.g. an API secret a
196    /// user saves, before storing it. The result is base64 text; only
197    /// [`AppState::decrypt`] with the same key reads it back, and any change
198    /// to it is detected. Rotating `APP_KEY` makes old values unreadable.
199    ///
200    /// ```
201    /// # use renox::prelude::*;
202    /// # fn demo(state: AppState) -> Result {
203    /// let sealed = state.encrypt("sk_live_123");
204    /// assert_eq!(state.decrypt(&sealed)?, "sk_live_123");
205    /// # Ok(()) }
206    /// ```
207    pub fn encrypt(&self, plain: &str) -> String {
208        crate::crypto::seal(&self.key, plain)
209    }
210
211    /// Reads a value from [`AppState::encrypt`]. Fails if it was changed or
212    /// sealed with another key.
213    pub fn decrypt(&self, sealed: &str) -> Result<String> {
214        Ok(crate::crypto::open(&self.key, sealed)?)
215    }
216}
217
218impl AppState {
219    /// A disk the app added with [`App::disk`](crate::App::disk), e.g.
220    /// `state.disk_named("backups")?`; an unknown name is an error (500).
221    pub fn disk_named(&self, name: &str) -> Result<&Storage> {
222        self.disks.get(name).ok_or_else(|| {
223            anyhow::anyhow!("no disk named `{name}`: add it with `App::disk(\"{name}\", …)`").into()
224        })
225    }
226}