better_auth_core/plugin_runtime/fields/
device.rs1use 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 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 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 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 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 #[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 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 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 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}