Skip to main content

better_auth_core/plugin_runtime/fields/
device.rs

1use super::*;
2use crate::id::IdGeneration;
3use crate::user_fields::UserFieldConfig;
4use crate::{DeviceCode, DeviceCodeOwnership, DeviceCodeWhere, WhereMode, WhereOperator};
5
6fn native_name(name: &str) -> bool {
7    crate::store::schema::core_fields(EntityRole::DeviceCode)
8        .iter()
9        .any(|field| field.name == name)
10        || matches!(
11            name,
12            "deviceCode"
13                | "userCode"
14                | "userId"
15                | "expiresAt"
16                | "lastPolledAt"
17                | "pollingInterval"
18                | "clientId"
19        )
20}
21
22pub(super) fn validate_fields(fields: &UserConfig) -> AuthResult<()> {
23    for (name, field) in fields.fields() {
24        let storage = resolve_field_name(field.field_name.as_deref(), name);
25        if name == "scope" {
26            if !matches!(field.field_type, UserFieldType::String)
27                || field.references.is_some()
28                || storage != name
29            {
30                return Err(AuthError::config(
31                    "DeviceCode scope requires its ordinary string column without reference or field-name replacement",
32                ));
33            }
34        } else if native_name(name) || native_name(storage) {
35            return Err(AuthError::config(format!(
36                "DeviceCode additional field {name} cannot replace native field {storage}"
37            )));
38        }
39    }
40    Ok(())
41}
42
43impl ModelFields {
44    /// Bind one ownership condition without invoking application field callbacks.
45    /// The returned condition uses its physical storage field name.
46    /// The third tuple item retains the original operand for adapter-specific conversion.
47    pub fn device_code_ownership_query(
48        &self,
49        ownership: &DeviceCodeOwnership,
50        policy: &IdGeneration,
51    ) -> AuthResult<(DeviceCodeWhere, &UserFieldConfig, Value)> {
52        static CLIENT_ID: LazyLock<UserFieldConfig> = LazyLock::new(UserFieldConfig::default);
53        let (mut query, logical, field) = match ownership {
54            DeviceCodeOwnership::ClientId(client) => (
55                DeviceCodeWhere::new("clientId", client.clone()),
56                "clientId",
57                &*CLIENT_ID,
58            ),
59            DeviceCodeOwnership::FieldEquals { field, value } => {
60                let (logical, config) = self.device_code_ownership_field(field, value)?;
61                (DeviceCodeWhere::new(field, value.clone()), logical, config)
62            }
63            DeviceCodeOwnership::FieldIn { field, values }
64            | DeviceCodeOwnership::FieldNotIn { field, values } => {
65                let (logical, config) = self.device_code_ownership_set_field(field, values)?;
66                (
67                    DeviceCodeWhere {
68                        field: field.clone(),
69                        operator: if matches!(ownership, DeviceCodeOwnership::FieldIn { .. }) {
70                            WhereOperator::In
71                        } else {
72                            WhereOperator::NotIn
73                        },
74                        value: values.clone().into(),
75                        mode: WhereMode::Sensitive,
76                    },
77                    logical,
78                    config,
79                )
80            }
81            DeviceCodeOwnership::Where(query) => {
82                if query.operator == WhereOperator::In && !matches!(query.value, Value::Array(_)) {
83                    return Err(AuthError::internal("Value must be an array"));
84                }
85                let (logical, field) = self.declared_device_code_ownership_field(&query.field)?;
86                if field.references.is_some()
87                    && !(field.references_id()
88                        && matches!(
89                            field.field_type,
90                            UserFieldType::String | UserFieldType::Json | UserFieldType::Date
91                        ))
92                {
93                    return Err(AuthError::config(
94                        "DeviceCode Where reference fields require the String, Json, or Date type and an id target",
95                    ));
96                }
97                (query.clone(), logical, field)
98            }
99        };
100        let original = query.value.clone();
101        if field.references_id() && matches!(policy, IdGeneration::Serial) {
102            query.value = crate::id::serial_reference_query_value(query.value)?;
103        }
104        query.value = crate::user_query::bind_filter(field, &query.value)?;
105        query.field = resolve_field_name(field.field_name.as_deref(), logical).to_owned();
106        Ok((query, field, original))
107    }
108
109    fn declared_device_code_ownership_field(
110        &self,
111        name: &str,
112    ) -> AuthResult<(&str, &UserFieldConfig)> {
113        static SCOPE: LazyLock<UserFieldConfig> = LazyLock::new(|| UserFieldConfig {
114            required: Some(false),
115            ..Default::default()
116        });
117        crate::user_query::declared_field(name, self.fields(EntityRole::DeviceCode))
118            .or_else(|| (name == "scope").then(|| ("scope", &*SCOPE)))
119            .ok_or_else(|| {
120                AuthError::config(format!(
121                    "DeviceCode ownership field {name} is not registered"
122                ))
123            })
124    }
125
126    /// Resolve and validate the supported scalar DeviceCode ownership query without running field callbacks.
127    pub fn device_code_ownership_field(
128        &self,
129        name: &str,
130        value: &Value,
131    ) -> AuthResult<(&str, &UserFieldConfig)> {
132        let (logical, field) = self.declared_device_code_ownership_field(name)?;
133        validate_reference(field)?;
134        if !matches!(
135            field.field_type,
136            UserFieldType::String | UserFieldType::Number | UserFieldType::Boolean
137        ) {
138            return Err(AuthError::config(
139                "DeviceCode FieldEquals supports only declared string, number, and boolean fields",
140            ));
141        }
142        if !matches!(
143            value,
144            Value::Null
145                | Value::Bool(_)
146                | Value::String(_)
147                | Value::Utf16String(_)
148                | Value::Number(_)
149        ) {
150            return Err(AuthError::config(
151                "DeviceCode FieldEquals requires a scalar null, string, number, or boolean value",
152            ));
153        }
154        Ok((logical, field))
155    }
156
157    /// Validate DeviceCode ownership candidates without invoking field input or output callbacks.
158    pub fn device_code_ownership_set_field(
159        &self,
160        name: &str,
161        values: &[Value],
162    ) -> AuthResult<(&str, &UserFieldConfig)> {
163        let (logical, field) = self.declared_device_code_ownership_field(name)?;
164        validate_reference(field)?;
165        if !matches!(
166            field.field_type,
167            UserFieldType::String | UserFieldType::Number
168        ) {
169            return Err(AuthError::config(
170                "DeviceCode field sets support only declared string and number fields",
171            ));
172        }
173        if values.iter().any(|value| {
174            !matches!(
175                value,
176                Value::Null | Value::String(_) | Value::Utf16String(_) | Value::Number(_)
177            )
178        }) {
179            return Err(AuthError::config(
180                "DeviceCode field sets require scalar null, string, or number candidates",
181            ));
182        }
183        Ok((logical, field))
184    }
185
186    /// Prepare scope and declared application fields without changing credential or owner bindings.
187    pub async fn device_code_fields_for_storage(
188        &self,
189        scope: SchemaValue<Option<String>>,
190        additional_fields: FieldMap,
191        create: bool,
192    ) -> AuthResult<FieldMap> {
193        self.device_code_fields_with_binding(scope, additional_fields, create, |_, _, value| {
194            Ok(value)
195        })
196        .await
197    }
198
199    /// Bind scope and application fields before omitting callback Undefined values.
200    #[doc(hidden)]
201    pub async fn device_code_fields_with_binding(
202        &self,
203        scope: SchemaValue<Option<String>>,
204        mut additional_fields: FieldMap,
205        create: bool,
206        bind: impl Fn(&str, &UserFieldConfig, Value) -> AuthResult<Value>,
207    ) -> AuthResult<FieldMap> {
208        // The typed scope owns omission too; application fields cannot supply an omitted native value.
209        let _ = additional_fields.shift_remove("scope");
210        let core = (!scope.is_undefined())
211            .then(|| scope.into_field_value())
212            .into_iter()
213            .map(|value| ("scope".into(), value))
214            .collect();
215        self.fields(EntityRole::DeviceCode)
216            .organization_storage_fields_with_binding(core, additional_fields, create, bind)
217            .await
218    }
219
220    pub(crate) fn take_device_code_scope(
221        fields: &mut FieldMap,
222    ) -> Option<SchemaValue<Option<String>>> {
223        optional_string(fields, "scope")
224    }
225
226    /// Project stored memory fields while retaining the original credential and authorization state.
227    pub async fn project_device_codes(
228        &self,
229        mut rows: Vec<DeviceCode>,
230    ) -> AuthResult<Vec<DeviceCode>> {
231        let records = rows
232            .iter()
233            .map(|row| {
234                let mut storage = row.additional_fields.clone();
235                if !row.scope.is_undefined() {
236                    let _ = storage.insert("scope".into(), row.scope.field_value());
237                }
238                AdapterRecord::new(FieldMap::new(), storage)
239            })
240            .collect();
241        let output = self
242            .fields(EntityRole::DeviceCode)
243            .project_memory_adapter_records(records)
244            .await?;
245        for (row, output) in rows.iter_mut().zip(output) {
246            self.assign_device_code_output(row, output);
247        }
248        Ok(rows)
249    }
250
251    /// Project extracted Device fields without replacing the raw record's typed bindings.
252    pub async fn project_device_code_records(
253        &self,
254        mut rows: Vec<DeviceCode>,
255        records: Vec<AdapterRecord>,
256        capabilities: crate::user_fields::FieldOutputCapabilities,
257        supports_native_dates: bool,
258    ) -> AuthResult<Vec<DeviceCode>> {
259        let fields = self.fields(EntityRole::DeviceCode);
260        let output = fields
261            .project_adapter_records_with_capabilities(records, capabilities, supports_native_dates)
262            .await?;
263        for (row, output) in rows.iter_mut().zip(output) {
264            self.assign_device_code_output(row, output);
265        }
266        Ok(rows)
267    }
268
269    pub(crate) fn assign_device_code_output(&self, row: &mut DeviceCode, mut output: FieldMap) {
270        if self
271            .fields(EntityRole::DeviceCode)
272            .fields()
273            .contains_key("scope")
274        {
275            row.scope = output
276                .shift_remove("scope")
277                .map(SchemaValue::from_field)
278                .unwrap_or_default();
279        }
280        row.additional_fields = output;
281    }
282}
283
284fn validate_reference(field: &UserFieldConfig) -> AuthResult<()> {
285    if field.references.is_some()
286        && !(field.references_id() && matches!(field.field_type, UserFieldType::String))
287    {
288        return Err(AuthError::config(
289            "DeviceCode ownership reference fields require the String type and an id target",
290        ));
291    }
292    Ok(())
293}