Skip to main content

better_auth_core/
user_fields.rs

1//! Application user fields at the input, storage, and public output boundaries.
2
3use crate::store::schema::resolve_field_name;
4use crate::{AuthError, AuthResult};
5use crate::{FieldMap, FieldValue as Value};
6use indexmap::IndexMap;
7use std::sync::{Arc, LazyLock};
8mod adapter;
9#[cfg(test)]
10mod factory_tests;
11#[cfg(test)]
12mod input_binding_tests;
13pub use adapter::FieldOutputCapabilities;
14mod batch;
15pub(crate) use batch::{
16    project_fields_batches_then, project_fields_then, project_source_fields_batches_then,
17    project_source_fields_then,
18};
19pub use record::AdapterRecord;
20pub(crate) use record::project_adapter_value;
21mod organization;
22pub(crate) use organization::assign_output;
23mod output;
24mod record;
25mod transform;
26mod user_record;
27pub use transform::UserFieldTransform;
28pub(crate) use user_record::USER_FIELDS;
29
30/// Synchronous public input validator. Return the validated value or a public validation message.
31pub type UserFieldValidator = Arc<dyn Fn(Value) -> AuthResult<Value> + Send + Sync>;
32
33/// Synchronous default or update factory. Errors stop field processing before persistence.
34pub type UserFieldFactory = Arc<dyn Fn() -> AuthResult<Value> + Send + Sync>;
35
36/// Referenced model and logical field, shared with the application's migration configuration.
37#[derive(Clone, Debug)]
38pub struct UserFieldReference {
39    /// Logical model name, or the literal name of an application-owned table.
40    pub model: String,
41    /// Logical field name, or the literal column name of an application-owned table.
42    pub field: String,
43}
44
45/// Storage type of an application user field.
46#[derive(Clone, Debug, Default)]
47pub enum UserFieldType {
48    /// UTF-8 string.
49    #[default]
50    String,
51    /// JSON number.
52    Number,
53    /// Boolean.
54    Boolean,
55    /// RFC 3339 timestamp.
56    Date,
57    /// JSON document.
58    Json,
59    /// Array of strings.
60    StringArray,
61    /// Array of numbers.
62    NumberArray,
63    /// One of the listed strings.
64    Enum(Vec<String>),
65}
66
67/// Input and output callbacks configured for an application field.
68#[derive(Clone, Default)]
69pub struct FieldTransforms {
70    /// Transform input at public parsing and storage boundaries.
71    /// Public parsing requires a synchronous callback; adapters can await async callbacks.
72    pub input: Option<UserFieldTransform>,
73    /// Transform stored values at adapter output boundaries.
74    pub output: Option<UserFieldTransform>,
75}
76
77/// Schema for an application-owned user field.
78#[derive(Clone, Default)]
79pub struct UserFieldConfig {
80    /// Storage type; route validation is supplied separately by `validator`.
81    pub field_type: UserFieldType,
82    /// User input requires `Some(true)`; organization input requires any value except `Some(false)`.
83    pub required: Option<bool>,
84    /// Permit public client input. Omission defaults to true.
85    pub input: Option<bool>,
86    /// Include the field in public user views. Omission defaults to true.
87    pub returned: Option<bool>,
88    /// Serialized application model field name. Omitted or empty names use the logical field name.
89    pub field_name: Option<String>,
90    /// Foreign-key metadata. References to `id` use the adapter's ID output conversion.
91    pub references: Option<UserFieldReference>,
92    /// Unique foreign keys select one related row; omission permits a related page.
93    pub unique: Option<bool>,
94    /// Constant creation default.
95    pub default_value: Option<Value>,
96    /// Creation default factory.
97    pub default_value_fn: Option<UserFieldFactory>,
98    /// Produce a stored value when an update omits this field.
99    pub on_update: Option<UserFieldFactory>,
100    /// Validate public input before persistence; takes precedence over the route input transform.
101    pub validator: Option<UserFieldValidator>,
102    /// Field callbacks. `None` preserves omission; `Some(Default::default())` is an empty container.
103    pub transform: Option<FieldTransforms>,
104}
105
106impl std::fmt::Debug for UserFieldConfig {
107    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        formatter
109            .debug_struct("UserFieldConfig")
110            .field("field_type", &self.field_type)
111            .field("required", &self.required)
112            .field("input", &self.input)
113            .field("returned", &self.returned)
114            .field("field_name", &self.field_name)
115            .finish_non_exhaustive()
116    }
117}
118
119/// Application user schema.
120#[derive(Clone, Default)]
121pub struct UserConfig {
122    /// Public field policies. `None` preserves omission; `Some(IndexMap::new())` is explicitly empty.
123    pub additional_fields: Option<IndexMap<String, UserFieldConfig>>,
124}
125
126impl UserFieldConfig {
127    /// Whether the public input boundary permits this field; defaults to true.
128    pub fn input(&self) -> bool {
129        self.input.unwrap_or(true)
130    }
131
132    /// Whether public views include this field; defaults to true.
133    pub fn returned(&self) -> bool {
134        self.returned.unwrap_or(true)
135    }
136
137    /// Configured input callback, without invoking the callback.
138    pub fn input_transform(&self) -> Option<&UserFieldTransform> {
139        self.transform
140            .as_ref()
141            .and_then(|transform| transform.input.as_ref())
142    }
143
144    /// Configured output callback, without invoking the callback.
145    pub fn output_transform(&self) -> Option<&UserFieldTransform> {
146        self.transform
147            .as_ref()
148            .and_then(|transform| transform.output.as_ref())
149    }
150
151    pub(crate) fn normalize_date(&self, value: &mut Value) -> AuthResult<()> {
152        if matches!(self.field_type, UserFieldType::Date)
153            && let Value::String(text) = value
154        {
155            *value = Value::Date(
156                crate::utils::date::parse_adapter_date(text)
157                    .map(crate::FieldDate::from)
158                    .unwrap_or_else(crate::FieldDate::invalid),
159            );
160        }
161        Ok(())
162    }
163
164    pub(crate) fn default_value(&self) -> AuthResult<Option<Value>> {
165        match &self.default_value_fn {
166            Some(factory) => factory().map(Some),
167            None => Ok(self.default_value.clone()),
168        }
169    }
170}
171
172/// JavaScript truthiness used by upstream protected-field checks.
173pub fn is_truthy(value: &Value) -> bool {
174    value.is_truthy()
175}
176
177pub(crate) fn fields_or_empty(
178    fields: &Option<IndexMap<String, UserFieldConfig>>,
179) -> &IndexMap<String, UserFieldConfig> {
180    static EMPTY: LazyLock<IndexMap<String, UserFieldConfig>> = LazyLock::new(IndexMap::new);
181    fields.as_ref().unwrap_or(&EMPTY)
182}
183
184impl UserConfig {
185    /// Runtime fields. An omitted map has no fields.
186    pub fn fields(&self) -> &IndexMap<String, UserFieldConfig> {
187        fields_or_empty(&self.additional_fields)
188    }
189
190    /// Configure fields, creating an explicitly present map when omitted.
191    pub fn fields_mut(&mut self) -> &mut IndexMap<String, UserFieldConfig> {
192        self.additional_fields.get_or_insert_default()
193    }
194
195    /// Parse public input. Updates omit missing fields and never apply creation defaults.
196    pub fn parse_input(&self, input: &FieldMap, create: bool) -> AuthResult<FieldMap> {
197        let mut parsed = FieldMap::new();
198        for (name, field) in self.fields() {
199            let value = if let Some(value) = input.get(name) {
200                if !field.input() {
201                    if create && let Some(default) = field.default_value()? {
202                        let _ = parsed.insert(name.clone(), default);
203                        continue;
204                    }
205                    if is_truthy(value) {
206                        return Err(AuthError::FieldInput {
207                            code: "FIELD_NOT_ALLOWED",
208                            message: format!("{name} is not allowed to be set"),
209                        });
210                    }
211                    continue;
212                }
213                if let Some(validate) = &field.validator {
214                    Some(
215                        validate(value.clone()).map_err(|error| AuthError::FieldInput {
216                            code: "VALIDATION_ERROR",
217                            message: error.to_string(),
218                        })?,
219                    )
220                } else if let Some(transform) = field.input_transform() {
221                    Some(transform.call_sync(value.clone())?)
222                } else {
223                    Some(value.clone())
224                }
225            } else if create {
226                if let Some(default) = field.default_value()? {
227                    Some(default)
228                } else if field.required == Some(true) {
229                    return Err(AuthError::FieldInput {
230                        code: "MISSING_FIELD",
231                        message: format!("{name} is required"),
232                    });
233                } else {
234                    continue;
235                }
236            } else {
237                continue;
238            };
239            if let Some(value) = value.filter(|value| !value.is_undefined()) {
240                let _ = parsed.insert(name.clone(), value);
241            }
242        }
243        Ok(parsed)
244    }
245
246    /// Parse an OAuth provider profile without trusting protected application fields.
247    pub fn parse_provider_input(&self, profile: &FieldMap, create: bool) -> AuthResult<FieldMap> {
248        let allowed = profile
249            .iter()
250            .filter(|(name, _)| self.fields().get(*name).is_some_and(|field| field.input()))
251            .map(|(name, value)| (name.clone(), value.clone()))
252            .collect();
253        self.parse_input(&allowed, create)
254    }
255
256    /// Map configured fields to application storage columns and await adapter transforms.
257    pub async fn storage_fields(&self, input: FieldMap, create: bool) -> AuthResult<FieldMap> {
258        self.storage_fields_async(input, create, false, |_, _, value| Ok(value))
259            .await
260    }
261
262    async fn storage_fields_async(
263        &self,
264        input: FieldMap,
265        create: bool,
266        preserve_id: bool,
267        bind: impl Fn(&str, &UserFieldConfig, Value) -> AuthResult<Value>,
268    ) -> AuthResult<FieldMap> {
269        let mut output = FieldMap::new();
270        for (name, field) in self.fields() {
271            if preserve_id && name == "id" {
272                continue;
273            }
274            if let Some(value) = field.storage_input(input.get(name), create).await? {
275                let storage = resolve_field_name(field.field_name.as_deref(), name);
276                let value = bind(storage, field, value)?;
277                if !value.is_undefined() {
278                    let _ = output.insert(storage.to_owned(), value);
279                }
280            }
281        }
282        Ok(output)
283    }
284}
285
286impl UserFieldConfig {
287    async fn storage_input(
288        &self,
289        input: Option<&Value>,
290        create: bool,
291    ) -> AuthResult<Option<Value>> {
292        let Some(value) = self.storage_value(input, create)? else {
293            return Ok(None);
294        };
295        let value = match self.input_transform() {
296            Some(transform) => transform.call(value).await,
297            None => Ok(value),
298        }?;
299        // Serial references convert callback Undefined to NaN before final omission.
300        Ok(Some(value))
301    }
302
303    // The option skips a field; Undefined can still reach a configured callback.
304    fn storage_value(&self, input: Option<&Value>, create: bool) -> AuthResult<Option<Value>> {
305        let mut value = input.cloned().unwrap_or_default();
306        if value.is_undefined()
307            && if create {
308                !self.has_storage_default() && self.input_transform().is_none()
309            } else {
310                self.on_update.is_none()
311            }
312        {
313            return Ok(None);
314        }
315        self.normalize_date(&mut value)?;
316        if create
317            && (value.is_undefined() || (self.required == Some(true) && value.is_null()))
318            && self.has_storage_default()
319            && let Some(default) = self.default_value()?
320        {
321            value = default;
322        }
323        if !create
324            && value.is_undefined()
325            && let Some(update) = &self.on_update
326        {
327            value = update()?;
328        }
329        Ok(Some(value))
330    }
331
332    pub(crate) fn has_storage_default(&self) -> bool {
333        self.default_value_fn.is_some()
334            || self
335                .default_value
336                .as_ref()
337                .is_some_and(|value| !value.is_undefined())
338    }
339}