Skip to main content

better_auth_core/user_fields/
output.rs

1use crate::store::schema::resolve_field_name;
2use crate::{AuthRecordFields, FromFieldMap, SchemaField};
3use crate::{AuthResult, entity::AuthUser, plugin::MetadataMap, wire::UserView};
4use crate::{FieldMap, FieldValue as Value};
5
6const PLUGIN_FIELDS: &[(&str, &[&str])] = &[
7    ("anonymous.enabled", &["isAnonymous"]),
8    (
9        "phone-number.enabled",
10        &["phoneNumber", "phoneNumberVerified"],
11    ),
12    ("username.enabled", &["username"]),
13    ("username.display.enabled", &["displayUsername"]),
14    ("two_factor.enabled", &["twoFactorEnabled"]),
15    (
16        "admin.enabled",
17        &["role", "banned", "banReason", "banExpires"],
18    ),
19];
20
21impl UserView {
22    pub(crate) fn active_plugin_fields(
23        metadata: &MetadataMap,
24    ) -> impl Iterator<Item = &'static str> + '_ {
25        PLUGIN_FIELDS
26            .iter()
27            .filter(move |(plugin, _)| {
28                metadata.get(*plugin).and_then(serde_json::Value::as_bool) == Some(true)
29            })
30            .flat_map(|(_, fields)| fields.iter().copied())
31    }
32
33    /// Build an enumeration-safe signup response without adapter transforms or persistence.
34    pub fn synthetic_output(
35        data: FieldMap,
36        config: &super::UserConfig,
37        metadata: &MetadataMap,
38    ) -> AuthResult<FieldMap> {
39        let mut output = FieldMap::new();
40        for name in [
41            "id",
42            "name",
43            "email",
44            "emailVerified",
45            "image",
46            "createdAt",
47            "updatedAt",
48        ] {
49            if let Some(value) = data.get(name) {
50                let _ = output.insert(name.into(), value.clone());
51            }
52        }
53        for (plugin, names) in PLUGIN_FIELDS {
54            if metadata.get(*plugin).and_then(serde_json::Value::as_bool) != Some(true) {
55                continue;
56            }
57            for name in *names {
58                let value = data.get(*name).cloned().unwrap_or_else(|| {
59                    if ["isAnonymous", "twoFactorEnabled", "banned"].contains(name) {
60                        Value::Bool(false)
61                    } else {
62                        Value::Null
63                    }
64                });
65                let _ = output.insert((*name).into(), value);
66            }
67        }
68        for (name, field) in config.fields() {
69            if !field.returned() {
70                let _ = output.remove(name);
71                continue;
72            }
73            let value = match data.get(name) {
74                Some(value) => Some(value.clone()),
75                None => field.default_value()?,
76            }
77            .or_else(|| (field.required != Some(true)).then_some(Value::Null));
78            if let Some(value) = value {
79                let _ = output.insert(name.clone(), value);
80            }
81        }
82        Ok(output)
83    }
84
85    /// Apply current `returned` restrictions to cached fields without repeating adapter transforms.
86    /// Upstream keeps fields from disabled plugins until the cache expires or its version changes.
87    pub(crate) fn filter_cached_fields(&mut self, config: &super::UserConfig) {
88        for (name, field) in config.fields() {
89            if field.returned() {
90                continue;
91            }
92            if let Some(fields) = &mut self.visible_fields {
93                let _ = fields.remove(name);
94            }
95            match name.as_str() {
96                "username" => self.username = None,
97                "displayUsername" => self.display_username = None,
98                _ => {}
99            }
100        }
101        self.additional_fields.retain(|name, _| {
102            config
103                .fields()
104                .get(name)
105                .is_none_or(|field| field.returned())
106        });
107    }
108
109    /// Construct the public user shape from the active application and plugin schemas.
110    /// Application model serialization is inspected only at this user boundary.
111    pub async fn with_fields<T: AuthUser>(
112        user: &T,
113        config: &super::UserConfig,
114        metadata: &MetadataMap,
115    ) -> AuthResult<Self> {
116        Self::project(user, config, metadata, true, true).await
117    }
118
119    pub async fn with_fields_for_adapter<T: AuthUser>(
120        user: &T,
121        config: &super::UserConfig,
122        metadata: &MetadataMap,
123        supports_native_json: bool,
124    ) -> AuthResult<Self> {
125        Self::project(user, config, metadata, true, supports_native_json).await
126    }
127
128    /// Apply adapter transforms once, then use the endpoint's field visibility.
129    pub async fn with_field_policies<T: AuthUser>(
130        user: &T,
131        adapter: &super::UserConfig,
132        endpoint: &super::UserConfig,
133        metadata: &MetadataMap,
134        supports_native_json: bool,
135    ) -> AuthResult<Self> {
136        let mut view = Self::project(user, adapter, metadata, false, supports_native_json).await?;
137        view.filter_cached_fields(endpoint);
138        Ok(view)
139    }
140
141    /// Apply adapter transforms and active schemas without removing application-only fields.
142    /// Use this view only for trusted callbacks, never for public responses.
143    pub async fn with_internal_fields<T: AuthUser>(
144        user: &T,
145        config: &super::UserConfig,
146        metadata: &MetadataMap,
147    ) -> AuthResult<Self> {
148        Self::project(user, config, metadata, false, true).await
149    }
150
151    pub async fn with_internal_fields_for_adapter<T: AuthUser>(
152        user: &T,
153        config: &super::UserConfig,
154        metadata: &MetadataMap,
155        supports_native_json: bool,
156    ) -> AuthResult<Self> {
157        Self::project(user, config, metadata, false, supports_native_json).await
158    }
159
160    /// Project a database result in row order, interleaving synchronous callbacks by field.
161    pub async fn with_internal_fields_many_for_adapter<T: AuthUser>(
162        users: &[T],
163        config: &super::UserConfig,
164        metadata: &MetadataMap,
165        supports_native_json: bool,
166    ) -> AuthResult<Vec<Self>> {
167        Self::project_many(
168            users,
169            config,
170            metadata,
171            false,
172            super::FieldOutputCapabilities::json_only(supports_native_json),
173            |_, _, _| Ok(None),
174        )
175        .await
176    }
177
178    /// Project database rows with raw callback values for unprojected additional fields.
179    /// `Some(Value::Null)` preserves a present null; `None` retains the serialized source.
180    /// Supplied non-reference extras use the adapter capabilities after their output callback.
181    /// Native fields and completed adapter output do not use this accessor.
182    pub async fn with_internal_fields_many_for_adapter_using<T: AuthUser>(
183        users: &[T],
184        config: &super::UserConfig,
185        metadata: &MetadataMap,
186        capabilities: super::FieldOutputCapabilities,
187        read_extra: impl Fn(&T, &str, &super::UserFieldConfig) -> AuthResult<Option<Value>> + Sync,
188    ) -> AuthResult<Vec<Self>> {
189        Self::project_many(users, config, metadata, false, capabilities, read_extra).await
190    }
191
192    async fn project<T: AuthUser>(
193        user: &T,
194        config: &super::UserConfig,
195        metadata: &MetadataMap,
196        public: bool,
197        supports_native_json: bool,
198    ) -> AuthResult<Self> {
199        // Projection preserves the one input row.
200        Ok(Self::project_many(
201            std::slice::from_ref(user),
202            config,
203            metadata,
204            public,
205            super::FieldOutputCapabilities::json_only(supports_native_json),
206            |_, _, _| Ok(None),
207        )
208        .await?
209        .remove(0))
210    }
211
212    async fn project_many<T: AuthUser>(
213        users: &[T],
214        config: &super::UserConfig,
215        metadata: &MetadataMap,
216        public: bool,
217        capabilities: super::FieldOutputCapabilities,
218        read_extra: impl Fn(&T, &str, &super::UserFieldConfig) -> AuthResult<Option<Value>> + Sync,
219    ) -> AuthResult<Vec<Self>> {
220        let mut rows = users
221            .iter()
222            .map(|user| {
223                let mut view = Self::from_model(user)?;
224                view.visible_fields = Some(
225                    Self::active_plugin_fields(metadata)
226                        .filter(|name| {
227                            user.field_presence()
228                                .is_none_or(|fields| fields.contains(*name))
229                        })
230                        .map(str::to_owned)
231                        .chain(
232                            ["name", "email", "image"]
233                                .into_iter()
234                                .filter(|name| {
235                                    user.field_presence()
236                                        .is_none_or(|fields| fields.contains(*name))
237                                })
238                                .map(str::to_owned),
239                        )
240                        .collect(),
241                );
242                view.additional_fields.clear();
243                let model = if !config.fields().is_empty() && user.projected_fields().is_none() {
244                    Some(user.field_values()?)
245                } else {
246                    None
247                };
248                Ok((user, view, model))
249            })
250            .collect::<AuthResult<Vec<_>>>()?;
251        let read_extra = &read_extra;
252        super::batch::project_fields(
253            &mut rows,
254            config.fields(),
255            |(user, view, model), name, field| {
256                Box::pin(async move {
257                    let value = if let Some(projected) = user.projected_fields() {
258                        match name {
259                            "id" => Some(view.id.field_value()),
260                            "name" => Some(view.name.field_value()),
261                            "image" => Some(view.image.field_value()),
262                            _ => projected.get(name).cloned(),
263                        }
264                        .unwrap_or_default()
265                    } else {
266                        let storage_name = resolve_field_name(field.field_name.as_deref(), name);
267                        let value = if Self::NATIVE_FIELDS.contains(&name) {
268                            None
269                        } else {
270                            read_extra(user, name, field)?
271                        };
272                        let raw_extra = value.is_some() && field.references.is_none();
273                        let value = value
274                            .or_else(|| {
275                                model
276                                    .as_ref()
277                                    .and_then(|model| model.get(storage_name))
278                                    .cloned()
279                            })
280                            .or_else(|| match (name, storage_name == name) {
281                                ("username", true) => {
282                                    Some(user.username().map(str::to_owned).into_field())
283                                }
284                                ("displayUsername", true) => {
285                                    Some(user.display_username().map(str::to_owned).into_field())
286                                }
287                                _ => None,
288                            });
289                        if raw_extra {
290                            field
291                                .adapter_output_from_raw(value.unwrap_or_default(), capabilities)
292                                .await?
293                        } else {
294                            field
295                                .adapter_output(
296                                    value.unwrap_or_default(),
297                                    capabilities.supports_native_json,
298                                )
299                                .await?
300                        }
301                    };
302                    if name == "username" || name == "displayUsername" {
303                        if let Some(fields) = &mut view.visible_fields {
304                            if !public || field.returned() {
305                                let _ = fields.insert(name.to_owned());
306                            } else {
307                                let _ = fields.remove(name);
308                            }
309                        }
310                        let typed = if !public || field.returned() {
311                            value.decode()?
312                        } else {
313                            None
314                        };
315                        if name == "username" {
316                            view.username = typed;
317                        } else {
318                            view.display_username = typed;
319                        }
320                    }
321                    if name == "name" || name == "image" {
322                        let target = if name == "name" {
323                            &mut view.name
324                        } else {
325                            &mut view.image
326                        };
327                        *target = crate::SchemaValue::from_field(value);
328                        if public
329                            && !field.returned()
330                            && let Some(fields) = &mut view.visible_fields
331                        {
332                            let _ = fields.remove(name);
333                        }
334                        return Ok(());
335                    }
336                    {
337                        let mut value = value;
338                        if !field.uses_id_output() {
339                            field.normalize_date(&mut value)?;
340                        }
341                        if !public || field.returned() {
342                            let _ = view.additional_fields.insert(name.to_owned(), value);
343                        }
344                    }
345                    Ok(())
346                })
347            },
348        )
349        .await?;
350        Ok(rows.into_iter().map(|(_, view, _)| view).collect())
351    }
352}
353
354impl UserView {
355    pub(crate) const NATIVE_FIELDS: &[&str] = &[
356        "id",
357        "name",
358        "email",
359        "emailVerified",
360        "image",
361        "createdAt",
362        "updatedAt",
363        "isAnonymous",
364        "phoneNumber",
365        "phoneNumberVerified",
366        "username",
367        "displayUsername",
368        "twoFactorEnabled",
369        "role",
370        "banned",
371        "banReason",
372        "banExpires",
373    ];
374
375    pub(crate) fn native_field_value(&self, name: &str) -> Option<Value> {
376        if matches!(name, "name" | "email" | "image")
377            && self
378                .visible_fields
379                .as_ref()
380                .is_some_and(|fields| !fields.contains(name))
381        {
382            return None;
383        }
384        let value = match name {
385            "id" if !self.id.is_undefined() => self.id.clone().into_field(),
386            "name" if !self.name.is_undefined() => self.name.clone().into_field(),
387            "email" => self.email.clone().into_field(),
388            "emailVerified" => self.email_verified.into_field(),
389            "image" if !self.image.is_undefined() => self.image.clone().into_field(),
390            "createdAt" => self.created_at.clone().into_field(),
391            "updatedAt" => self.updated_at.clone().into_field(),
392            "id" | "name" | "image" => return None,
393            _ => {
394                if self
395                    .visible_fields
396                    .as_ref()
397                    .is_some_and(|fields| !fields.contains(name))
398                {
399                    return None;
400                }
401                let value = match name {
402                    "isAnonymous" => self.is_anonymous.into_field(),
403                    "phoneNumber" => self.phone_number.clone().into_field(),
404                    "phoneNumberVerified" => self.phone_number_verified.into_field(),
405                    "username" => self.username.clone().into_field(),
406                    "displayUsername" => self.display_username.clone().into_field(),
407                    "twoFactorEnabled" => self.two_factor_enabled.into_field(),
408                    "role" => self.role.clone().into_field(),
409                    "banned" => self.banned.into_field(),
410                    "banReason" => self.ban_reason.clone().into_field(),
411                    "banExpires" => self.ban_expires.clone().into_field(),
412                    _ => return None,
413                };
414                if name == "isAnonymous" && value.is_null() && self.visible_fields.is_some() {
415                    return Some(Value::Bool(false));
416                }
417                if self.visible_fields.is_none()
418                    && value.is_null()
419                    && ["isAnonymous", "phoneNumber", "phoneNumberVerified"].contains(&name)
420                {
421                    return None;
422                }
423                value
424            }
425        };
426        Some(value)
427    }
428}
429
430impl From<UserView> for FieldMap {
431    fn from(user: UserView) -> Self {
432        let mut result: FieldMap = UserView::NATIVE_FIELDS
433            .iter()
434            .filter_map(|name| {
435                user.native_field_value(name)
436                    .map(|value| ((*name).into(), value))
437            })
438            .collect();
439        result.extend(user.additional_fields);
440        result
441    }
442}
443
444impl TryFrom<FieldMap> for UserView {
445    type Error = crate::AuthError;
446
447    fn try_from(mut fields: FieldMap) -> Result<Self, Self::Error> {
448        fn take<T: crate::SchemaField>(fields: &mut FieldMap, name: &str) -> AuthResult<T> {
449            fields.remove(name).unwrap_or_default().decode()
450        }
451        let visible_fields = Some(
452            PLUGIN_FIELDS
453                .iter()
454                .flat_map(|(_, names)| names.iter())
455                .filter(|name| fields.contains_key(**name))
456                .map(|name| (*name).to_owned())
457                .chain(
458                    ["name", "email", "image"]
459                        .into_iter()
460                        .filter(|name| fields.contains_key(*name))
461                        .map(str::to_owned),
462                )
463                .collect(),
464        );
465        Ok(Self {
466            id: crate::SchemaValue::from_field(fields.remove("id").unwrap_or_default()),
467            name: crate::SchemaValue::from_field(fields.remove("name").unwrap_or_default()),
468            email: take(&mut fields, "email")?,
469            email_verified: take(&mut fields, "emailVerified")?,
470            image: crate::SchemaValue::from_field(fields.remove("image").unwrap_or_default()),
471            created_at: take(&mut fields, "createdAt")?,
472            updated_at: take(&mut fields, "updatedAt")?,
473            is_anonymous: take(&mut fields, "isAnonymous")?,
474            phone_number: take(&mut fields, "phoneNumber")?,
475            phone_number_verified: take(&mut fields, "phoneNumberVerified")?,
476            // Keep schema-backed fields in the projected map for later cookie updates.
477            username: fields
478                .get("username")
479                .cloned()
480                .unwrap_or_default()
481                .decode()?,
482            display_username: fields
483                .get("displayUsername")
484                .cloned()
485                .unwrap_or_default()
486                .decode()?,
487            two_factor_enabled: take(&mut fields, "twoFactorEnabled")?,
488            role: take(&mut fields, "role")?,
489            banned: take::<Option<bool>>(&mut fields, "banned")?.unwrap_or(false),
490            ban_reason: take(&mut fields, "banReason")?,
491            ban_expires: take(&mut fields, "banExpires")?,
492            metadata: fields.remove("metadata").unwrap_or(Value::Null),
493            visible_fields,
494            additional_fields: fields,
495        })
496    }
497}
498
499impl AuthRecordFields for UserView {
500    fn field_values(&self) -> AuthResult<FieldMap> {
501        let mut fields = FieldMap::from(self.clone());
502        let _ = fields.insert("metadata".into(), self.metadata.clone());
503        Ok(fields)
504    }
505
506    fn structured_clone(&self, context: &mut crate::StructuredCloneContext) -> AuthResult<Self> {
507        let mut user = self.clone();
508        user.id = context.clone_field(&self.id)?;
509        user.name = context.clone_field(&self.name)?;
510        user.image = context.clone_field(&self.image)?;
511        user.created_at = context.clone_field(&self.created_at)?;
512        user.updated_at = context.clone_field(&self.updated_at)?;
513        user.ban_expires = context.clone_field(&self.ban_expires)?;
514        user.metadata = context.clone_value(&self.metadata);
515        user.additional_fields = context.clone_map(&self.additional_fields);
516        Ok(user)
517    }
518}
519
520impl FromFieldMap for UserView {
521    fn from_field_values(fields: FieldMap) -> AuthResult<Self> {
522        Self::try_from(fields)
523    }
524}
525
526impl serde::Serialize for UserView {
527    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
528        crate::field_value::serde::map::serialize(&FieldMap::from(self.clone()), serializer)
529    }
530}
531
532impl<'de> serde::Deserialize<'de> for UserView {
533    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
534        use serde::de::Error;
535        let mut fields = crate::field_value::serde::map::deserialize(deserializer)?;
536        for name in ["createdAt", "updatedAt", "banExpires"] {
537            if let Some(Value::String(text)) = fields.get(name) {
538                let date = crate::utils::date::parse_adapter_date(text)
539                    .ok_or_else(|| D::Error::custom(format!("Invalid user date field `{name}`")))?;
540                let _ = fields.insert(name.into(), crate::FieldDate::from(date).into());
541            }
542        }
543        Self::from_field_values(fields).map_err(D::Error::custom)
544    }
545}