Skip to main content

better_auth_core/wire/
user.rs

1use super::*;
2/// Public user response shape.
3#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
4#[serde(try_from = "UserViewInput")]
5pub struct UserView {
6    pub id: String,
7    pub name: Option<String>,
8    pub email: Option<String>,
9    #[serde(rename = "emailVerified")]
10    pub email_verified: bool,
11    pub image: Option<String>,
12    #[serde(rename = "createdAt")]
13    #[serde(serialize_with = "crate::utils::datetime::serialize")]
14    pub created_at: DateTime<Utc>,
15    #[serde(rename = "updatedAt")]
16    #[serde(serialize_with = "crate::utils::datetime::serialize")]
17    pub updated_at: DateTime<Utc>,
18    #[serde(default, skip_serializing_if = "Option::is_none")]
19    pub username: Option<String>,
20    #[serde(
21        rename = "displayUsername",
22        default,
23        skip_serializing_if = "Option::is_none"
24    )]
25    pub display_username: Option<String>,
26    #[serde(
27        rename = "twoFactorEnabled",
28        default,
29        skip_serializing_if = "Option::is_none"
30    )]
31    pub two_factor_enabled: Option<bool>,
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub role: Option<String>,
34    #[serde(default, skip_serializing_if = "Option::is_none")]
35    pub banned: Option<bool>,
36    #[serde(rename = "banReason", default, skip_serializing_if = "Option::is_none")]
37    pub ban_reason: Option<String>,
38    #[serde(
39        rename = "banExpires",
40        default,
41        skip_serializing_if = "Option::is_none"
42    )]
43    #[serde(serialize_with = "crate::utils::datetime::serialize_optional")]
44    pub ban_expires: Option<DateTime<Utc>>,
45    #[serde(
46        rename = "isAnonymous",
47        default,
48        skip_serializing_if = "Option::is_none"
49    )]
50    pub is_anonymous: Option<bool>,
51    #[serde(
52        rename = "phoneNumber",
53        default,
54        skip_serializing_if = "Option::is_none"
55    )]
56    pub phone_number: Option<String>,
57    #[serde(
58        rename = "phoneNumberVerified",
59        default,
60        skip_serializing_if = "Option::is_none"
61    )]
62    pub phone_number_verified: Option<bool>,
63    #[serde(
64        rename = "lastLoginMethod",
65        default,
66        skip_serializing_if = "Option::is_none"
67    )]
68    pub last_login_method: Option<String>,
69    /// Nullable fields contributed by an enabled plugin's output schema.
70    #[serde(
71        flatten,
72        default,
73        deserialize_with = "crate::utils::json::deserialize_btree_map"
74    )]
75    pub extension_fields: std::collections::BTreeMap<String, serde_json::Value>,
76    #[serde(skip)]
77    pub metadata: serde_json::Value,
78    /// Fields absent from an authenticated cache projection.
79    #[serde(skip)]
80    pub omitted_fields: std::collections::BTreeSet<String>,
81}
82
83impl Serialize for UserView {
84    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
85        use serde::ser::SerializeMap;
86        let mut map = serializer.serialize_map(None)?;
87        macro_rules! entry {
88            ($name:literal, $value:expr) => {
89                if !self.omitted_fields.contains($name) {
90                    map.serialize_entry($name, $value)?;
91                }
92            };
93        }
94        entry!("id", &self.id);
95        entry!("name", &self.name);
96        entry!("email", &self.email);
97        if let Some(value) = self.extension_fields.get("emailVerified") {
98            entry!("emailVerified", value);
99        } else {
100            entry!("emailVerified", &self.email_verified);
101        }
102        entry!("image", &self.image);
103        entry!(
104            "createdAt",
105            &crate::utils::datetime::json_date_millis(self.created_at.timestamp_millis())
106        );
107        entry!(
108            "updatedAt",
109            &crate::utils::datetime::json_date_millis(self.updated_at.timestamp_millis())
110        );
111        if let Some(value) = &self.username {
112            entry!("username", value);
113        }
114        if let Some(value) = &self.display_username {
115            entry!("displayUsername", value);
116        }
117        if let Some(value) = &self.two_factor_enabled {
118            entry!("twoFactorEnabled", value);
119        }
120        if let Some(value) = &self.role {
121            entry!("role", value);
122        }
123        if let Some(value) = &self.banned {
124            entry!("banned", value);
125        }
126        if let Some(value) = &self.ban_reason {
127            entry!("banReason", value);
128        }
129        if let Some(value) = &self.ban_expires {
130            entry!(
131                "banExpires",
132                &crate::utils::datetime::json_date_millis(value.timestamp_millis())
133            );
134        }
135        if let Some(value) = &self.is_anonymous {
136            entry!("isAnonymous", value);
137        }
138        if let Some(value) = &self.phone_number {
139            entry!("phoneNumber", value);
140        }
141        if let Some(value) = &self.phone_number_verified {
142            entry!("phoneNumberVerified", value);
143        }
144        if let Some(value) = &self.last_login_method {
145            entry!("lastLoginMethod", value);
146        }
147        for (name, value) in &self.extension_fields {
148            if name != "emailVerified" && !self.omitted_fields.contains(name) {
149                map.serialize_entry(name, value)?;
150            }
151        }
152        map.end()
153    }
154}
155
156#[derive(Deserialize)]
157pub(in crate::wire) struct UserViewInput {
158    pub(in crate::wire) id: String,
159    pub(in crate::wire) name: Option<String>,
160    pub(in crate::wire) email: Option<String>,
161    #[serde(rename = "emailVerified")]
162    pub(in crate::wire) email_verified: serde_json::Value,
163    pub(in crate::wire) image: Option<String>,
164    #[serde(rename = "createdAt")]
165    #[serde(serialize_with = "crate::utils::datetime::serialize")]
166    pub(in crate::wire) created_at: DateTime<Utc>,
167    #[serde(rename = "updatedAt")]
168    #[serde(serialize_with = "crate::utils::datetime::serialize")]
169    pub(in crate::wire) updated_at: DateTime<Utc>,
170    #[serde(default, skip_serializing_if = "Option::is_none")]
171    pub(in crate::wire) username: Option<String>,
172    #[serde(
173        rename = "displayUsername",
174        default,
175        skip_serializing_if = "Option::is_none"
176    )]
177    pub(in crate::wire) display_username: Option<String>,
178    #[serde(
179        rename = "twoFactorEnabled",
180        default,
181        skip_serializing_if = "Option::is_none"
182    )]
183    pub(in crate::wire) two_factor_enabled: Option<bool>,
184    #[serde(default, skip_serializing_if = "Option::is_none")]
185    pub(in crate::wire) role: Option<String>,
186    #[serde(default, skip_serializing_if = "Option::is_none")]
187    pub(in crate::wire) banned: Option<bool>,
188    #[serde(rename = "banReason", default, skip_serializing_if = "Option::is_none")]
189    pub(in crate::wire) ban_reason: Option<String>,
190    #[serde(
191        rename = "banExpires",
192        default,
193        skip_serializing_if = "Option::is_none"
194    )]
195    #[serde(serialize_with = "crate::utils::datetime::serialize_optional")]
196    pub(in crate::wire) ban_expires: Option<DateTime<Utc>>,
197    #[serde(
198        rename = "isAnonymous",
199        default,
200        skip_serializing_if = "Option::is_none"
201    )]
202    pub(in crate::wire) is_anonymous: Option<bool>,
203    #[serde(
204        rename = "phoneNumber",
205        default,
206        skip_serializing_if = "Option::is_none"
207    )]
208    pub(in crate::wire) phone_number: Option<String>,
209    #[serde(
210        rename = "phoneNumberVerified",
211        default,
212        skip_serializing_if = "Option::is_none"
213    )]
214    pub(in crate::wire) phone_number_verified: Option<bool>,
215    #[serde(
216        rename = "lastLoginMethod",
217        default,
218        skip_serializing_if = "Option::is_none"
219    )]
220    pub(in crate::wire) last_login_method: Option<String>,
221    /// Nullable fields contributed by an enabled plugin's output schema.
222    #[serde(
223        flatten,
224        default,
225        deserialize_with = "crate::utils::json::deserialize_btree_map"
226    )]
227    pub(in crate::wire) extension_fields: std::collections::BTreeMap<String, serde_json::Value>,
228    #[serde(skip)]
229    pub(in crate::wire) metadata: serde_json::Value,
230}
231
232impl TryFrom<UserViewInput> for UserView {
233    type Error = &'static str;
234    fn try_from(mut input: UserViewInput) -> Result<Self, Self::Error> {
235        let email_verified = match &input.email_verified {
236            serde_json::Value::Bool(value) => *value,
237            serde_json::Value::String(value) => !value.is_empty(),
238            _ => {
239                return Err("User verification output must be a boolean or retained SQLite string");
240            }
241        };
242        if !input.email_verified.is_boolean() {
243            drop(
244                input
245                    .extension_fields
246                    .insert("emailVerified".into(), input.email_verified),
247            );
248        }
249        Ok(Self {
250            email_verified,
251            extension_fields: input.extension_fields,
252            id: input.id,
253            name: input.name,
254            email: input.email,
255            image: input.image,
256            created_at: input.created_at,
257            updated_at: input.updated_at,
258            username: input.username,
259            display_username: input.display_username,
260            two_factor_enabled: input.two_factor_enabled,
261            role: input.role,
262            banned: input.banned,
263            ban_reason: input.ban_reason,
264            ban_expires: input.ban_expires,
265            is_anonymous: input.is_anonymous,
266            phone_number: input.phone_number,
267            phone_number_verified: input.phone_number_verified,
268            last_login_method: input.last_login_method,
269            metadata: input.metadata,
270            omitted_fields: std::collections::BTreeSet::default(),
271        })
272    }
273}
274
275impl<T: AuthUser> From<&T> for UserView {
276    fn from(user: &T) -> Self {
277        let mut view = Self {
278            id: user.id().into_owned(),
279            name: user.name().map(str::to_owned),
280            email: user.email().map(str::to_owned),
281            email_verified: user.email_verified(),
282            image: user.image().map(str::to_owned),
283            created_at: user.created_at(),
284            updated_at: user.updated_at(),
285            username: user.username().map(str::to_owned),
286            display_username: user.display_username().map(str::to_owned),
287            two_factor_enabled: user.two_factor_enabled_value(),
288            role: user.role().map(str::to_owned),
289            banned: user.banned_value(),
290            ban_reason: user.ban_reason().map(str::to_owned),
291            ban_expires: user.ban_expires(),
292            is_anonymous: user.is_anonymous(),
293            phone_number: user.phone_number().map(str::to_owned),
294            phone_number_verified: user.phone_number_verified(),
295            last_login_method: user.last_login_method().map(str::to_owned),
296            extension_fields: std::collections::BTreeMap::default(),
297            metadata: user.metadata().clone(),
298            omitted_fields: std::collections::BTreeSet::default(),
299        };
300        if let Some(value) = user
301            .adapter_snapshot()
302            .and_then(|output| output.values().get("emailVerified"))
303            && !value.is_boolean()
304        {
305            drop(
306                view.extension_fields
307                    .insert("emailVerified".into(), value.clone()),
308            );
309        }
310        view
311    }
312}
313
314impl AuthUser for UserView {
315    fn retained_user_view(&self) -> Option<&UserView> {
316        Some(self)
317    }
318    fn additional_fields(&self) -> crate::field_policy::FieldOutput {
319        self.extension_fields.clone().into_iter().collect()
320    }
321    fn id(&self) -> Cow<'_, str> {
322        Cow::Borrowed(&self.id)
323    }
324    fn email(&self) -> Option<&str> {
325        self.email.as_deref()
326    }
327    fn name(&self) -> Option<&str> {
328        self.name.as_deref()
329    }
330    fn email_verified(&self) -> bool {
331        self.email_verified
332    }
333    fn image(&self) -> Option<&str> {
334        self.image.as_deref()
335    }
336    fn created_at(&self) -> DateTime<Utc> {
337        self.created_at
338    }
339    fn updated_at(&self) -> DateTime<Utc> {
340        self.updated_at
341    }
342    fn username(&self) -> Option<&str> {
343        self.username.as_deref()
344    }
345    fn display_username(&self) -> Option<&str> {
346        self.display_username.as_deref()
347    }
348    fn two_factor_enabled(&self) -> bool {
349        self.two_factor_enabled.unwrap_or(false)
350    }
351
352    fn two_factor_enabled_value(&self) -> Option<bool> {
353        self.two_factor_enabled
354    }
355    fn role(&self) -> Option<&str> {
356        self.role.as_deref()
357    }
358    fn banned(&self) -> bool {
359        self.banned.unwrap_or(false)
360    }
361
362    fn banned_value(&self) -> Option<bool> {
363        self.banned
364    }
365    fn ban_reason(&self) -> Option<&str> {
366        self.ban_reason.as_deref()
367    }
368    fn ban_expires(&self) -> Option<DateTime<Utc>> {
369        self.ban_expires
370    }
371    fn is_anonymous(&self) -> Option<bool> {
372        self.is_anonymous
373    }
374    fn phone_number(&self) -> Option<&str> {
375        self.phone_number.as_deref()
376    }
377    fn phone_number_verified(&self) -> Option<bool> {
378        self.phone_number_verified
379    }
380    fn last_login_method(&self) -> Option<&str> {
381        self.last_login_method.as_deref()
382    }
383    fn metadata(&self) -> &serde_json::Value {
384        &self.metadata
385    }
386}