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    /// Values every view gets (`App::share`).
93    pub(crate) shares: Arc<Vec<(String, crate::view::ShareFn)>>,
94    /// The app's notification channels (`App::channel`).
95    pub(crate) channels:
96        Arc<std::collections::HashMap<String, crate::auth::notifications::ChannelFn>>,
97    /// The app's own values (`App::provide`).
98    pub(crate) provided: crate::provided::ProvidedMap,
99    /// `/_renox/debug`'s recent requests, while developing locally.
100    pub(crate) inspector: Option<Arc<crate::inspector::Inspector>>,
101    /// Named rate limiters (`App::rate_limiter`).
102    pub(crate) limiters: Arc<std::collections::HashMap<String, crate::rate_limit::NamedLimiter>>,
103    /// The app's error reporters (`App::report`).
104    pub(crate) reporters: Arc<Vec<crate::report::ReportFn>>,
105    /// What tests asked to record instead of doing (`TestApp::fake_events`, …).
106    pub(crate) fakes: Arc<Fakes>,
107}
108
109/// A notification a test recorded instead of sending.
110#[derive(Debug, Clone)]
111#[non_exhaustive]
112pub struct SentNotification {
113    /// `Notification::kind`.
114    pub kind: &'static str,
115    /// Who it was sent to.
116    pub to: crate::auth::Recipient,
117}
118
119/// An event a test recorded instead of sending it to the open pages
120/// (`TestApp::fake_broadcasts`, `AppState::broadcast`).
121#[derive(Debug, Clone, PartialEq)]
122#[non_exhaustive]
123pub struct SentBroadcast {
124    /// The user whose pages it was for; `None` for every page.
125    pub user_id: Option<i64>,
126    /// The DOM event's name.
127    pub event: String,
128    /// Its data (the DOM event's `detail`).
129    pub data: serde_json::Value,
130}
131
132/// Recorders for `TestApp::fake_events`, `fake_notifications` and
133/// `fake_broadcasts`.
134#[derive(Default)]
135pub(crate) struct Fakes {
136    pub events: std::sync::Mutex<Option<Vec<Box<dyn std::any::Any + Send>>>>,
137    pub notifications: std::sync::Mutex<Option<Vec<SentNotification>>>,
138    pub broadcasts: std::sync::Mutex<Option<Vec<SentBroadcast>>>,
139}
140
141impl Fakes {
142    /// Records `event` if events are faked; returns whether it did.
143    pub(crate) fn record_event<E: Send + 'static>(&self, event: E) -> bool {
144        let mut events = self.events.lock().unwrap_or_else(|e| e.into_inner());
145        match events.as_mut() {
146            Some(list) => {
147                list.push(Box::new(event));
148                true
149            }
150            None => false,
151        }
152    }
153
154    /// Records a broadcast if broadcasts are faked; returns whether it did.
155    pub(crate) fn record_broadcast(&self, broadcast: SentBroadcast) -> bool {
156        let mut sent = self.broadcasts.lock().unwrap_or_else(|e| e.into_inner());
157        match sent.as_mut() {
158            Some(list) => {
159                list.push(broadcast);
160                true
161            }
162            None => false,
163        }
164    }
165
166    /// Records a notification if notifications are faked.
167    pub(crate) fn record_notification(
168        &self,
169        kind: &'static str,
170        to: &crate::auth::Recipient,
171    ) -> bool {
172        let mut sent = self.notifications.lock().unwrap_or_else(|e| e.into_inner());
173        match sent.as_mut() {
174            Some(list) => {
175                list.push(SentNotification {
176                    kind,
177                    to: to.clone(),
178                });
179                true
180            }
181            None => false,
182        }
183    }
184}
185
186impl AppState {
187    /// The URL path of a named route, e.g. `state.url("products.show", &[&id])`.
188    pub fn url(&self, name: &str, params: &[&dyn Display]) -> Result<String> {
189        Ok(self.routes.url(name, params)?)
190    }
191
192    /// Encrypts `plain` with `APP_KEY` (AES-256-GCM), e.g. an API secret a
193    /// user saves, before storing it. The result is base64 text; only
194    /// [`AppState::decrypt`] with the same key reads it back, and any change
195    /// to it is detected. Rotating `APP_KEY` makes old values unreadable.
196    ///
197    /// ```
198    /// # use renox::prelude::*;
199    /// # fn demo(state: AppState) -> Result {
200    /// let sealed = state.encrypt("sk_live_123");
201    /// assert_eq!(state.decrypt(&sealed)?, "sk_live_123");
202    /// # Ok(()) }
203    /// ```
204    pub fn encrypt(&self, plain: &str) -> String {
205        crate::crypto::seal(&self.key, plain)
206    }
207
208    /// Reads a value from [`AppState::encrypt`]. Fails if it was changed or
209    /// sealed with another key.
210    pub fn decrypt(&self, sealed: &str) -> Result<String> {
211        Ok(crate::crypto::open(&self.key, sealed)?)
212    }
213}
214
215impl AppState {
216    /// A disk the app added with [`App::disk`](crate::App::disk), e.g.
217    /// `state.disk_named("backups")?`; an unknown name is an error (500).
218    pub fn disk_named(&self, name: &str) -> Result<&Storage> {
219        self.disks.get(name).ok_or_else(|| {
220            anyhow::anyhow!("no disk named `{name}`: add it with `App::disk(\"{name}\", …)`").into()
221        })
222    }
223}