parse_rust_server/state.rs
1//! Server state: config plus the storage adapter.
2//!
3//! Concrete over `MongoAdapter` rather than generic or boxed. 0.1.0 is Mongo only, and a type
4//! parameter threaded through every handler would be noise until a second backend exists. The
5//! `StorageAdapter` trait is still what the pipelines are written against, so swapping this for a
6//! generic later is a change in one file rather than in every route.
7
8use std::sync::Arc;
9
10use parse_rust_mongo::MongoAdapter;
11use parse_rust_rest::AclScope;
12
13use crate::config::ServerConfig;
14use crate::sessions::SessionStore;
15
16#[derive(Clone)]
17pub struct AppState {
18 config: Arc<ServerConfig>,
19 storage: Arc<MongoAdapter>,
20 sessions: Arc<SessionStore>,
21}
22
23impl AppState {
24 pub fn new(config: ServerConfig, storage: MongoAdapter) -> Self {
25 Self {
26 config: Arc::new(config),
27 storage: Arc::new(storage),
28 sessions: Arc::new(SessionStore::default()),
29 }
30 }
31
32 pub fn config(&self) -> &ServerConfig {
33 &self.config
34 }
35
36 pub fn storage(&self) -> &MongoAdapter {
37 &self.storage
38 }
39
40 pub fn sessions(&self) -> &SessionStore {
41 &self.sessions
42 }
43
44 /// Create the indexes parse-server creates at boot.
45 ///
46 /// **The names are contract, not housekeeping.** Both adapters recover `duplicated_field` by
47 /// regex over the index name, and the Mongo regex matches only auto-generated `<field>_1`
48 /// names, so passing `None` here (which lets the driver auto-name) is what makes a username
49 /// collision surface as 202 `USERNAME_TAKEN` rather than a bare 137. Naming them ourselves
50 /// would silently change the error a client sees.
51 ///
52 /// Upstream gates each of these behind a `databaseOptions.createIndexUser*` flag. Those are
53 /// not modeled yet; the indexes are unconditional here, which is the default behavior.
54 pub async fn ensure_indexes(&self) -> Result<(), parse_rust_core::ParseError> {
55 use parse_rust_storage::StorageAdapter;
56 self.storage
57 .ensure_unique_index("_User", &["username"], None)
58 .await?;
59 self.storage
60 .ensure_unique_index("_User", &["email"], None)
61 .await?;
62 Ok(())
63 }
64
65 /// Resolve a session token to an ACL scope.
66 ///
67 /// **An unknown or expired token is an error, not anonymity.** Downgrading silently meant a
68 /// client whose session had gone continued to work as a public caller: writes succeeded
69 /// against public rows and reads returned public data, with no signal that authentication had
70 /// failed. Upstream answers `INVALID_SESSION_TOKEN` (209), and so does this.
71 pub fn scope_for_session(&self, token: &str) -> Result<AclScope, parse_rust_core::ParseError> {
72 match self.sessions.user_for(token) {
73 Some(object_id) => Ok(AclScope::User { object_id }),
74 None => Err(parse_rust_core::ParseError::new(
75 parse_rust_core::ErrorCode::InvalidSessionToken,
76 "Invalid session token",
77 )),
78 }
79 }
80}
81
82impl axum::extract::FromRef<AppState> for Arc<ServerConfig> {
83 fn from_ref(state: &AppState) -> Self {
84 state.config.clone()
85 }
86}