better_auth_core/plugin_runtime/fields/
passkey.rs1use super::*;
2use crate::Passkey;
3use crate::store::schema::core_fields;
4use crate::user_fields::UserFieldConfig;
5
6fn native_name(name: &str) -> bool {
7 core_fields(EntityRole::Passkey)
8 .iter()
9 .any(|field| field.name == name)
10 || matches!(
11 name,
12 "publicKey"
13 | "userId"
14 | "credentialID"
15 | "credentialId"
16 | "deviceType"
17 | "backedUp"
18 | "createdAt"
19 | "updatedAt"
20 )
21}
22
23pub(super) fn field_order(name: &str) -> u8 {
24 match name {
25 "name" => 0,
26 "aaguid" => 1,
27 _ => 2,
28 }
29}
30
31pub(super) fn validate_fields(fields: &UserConfig) -> AuthResult<()> {
32 for (name, field) in fields.fields() {
33 let storage = resolve_field_name(field.field_name.as_deref(), name);
34 if matches!(name.as_str(), "name" | "aaguid") {
35 if !matches!(
36 field.field_type,
37 UserFieldType::String | UserFieldType::Enum(_) | UserFieldType::Json
38 ) || field.references.is_some()
39 || (native_name(storage) && storage != name)
40 {
41 return Err(AuthError::config(format!(
42 "Passkey {name} requires a string, enum, or JSON declaration without a reference or a different native field"
43 )));
44 }
45 } else if native_name(name) || native_name(storage) {
46 return Err(AuthError::config(format!(
47 "Passkey additional field {name} cannot replace native field {storage}"
48 )));
49 }
50 for native in ["name", "aaguid"] {
51 let column = storage_name(fields, native);
52 if !matches!(name.as_str(), "name" | "aaguid") && (name == column || storage == column)
53 {
54 return Err(AuthError::config(format!(
55 "Passkey field {name} conflicts with {native} storage column {column}"
56 )));
57 }
58 }
59 }
60 Ok(())
61}
62
63fn storage_name<'a>(fields: &'a UserConfig, name: &'a str) -> &'a str {
64 resolve_field_name(
65 fields
66 .fields()
67 .get(name)
68 .and_then(|field| field.field_name.as_deref()),
69 name,
70 )
71}
72
73pub(crate) struct PasskeyFieldPatch {
74 pub name: Option<SchemaValue<Option<String>>>,
75 pub aaguid: Option<SchemaValue<Option<String>>>,
76 pub additional_fields: FieldMap,
77}
78
79impl PasskeyFieldPatch {
80 pub(crate) fn apply(self, row: &mut Passkey) {
81 if let Some(name) = self.name {
82 row.name = name;
83 }
84 if let Some(aaguid) = self.aaguid {
85 row.aaguid = aaguid;
86 }
87 row.additional_fields.extend(self.additional_fields);
88 }
89}
90
91impl ModelFields {
92 pub(crate) async fn passkey_fields_for_storage(
93 &self,
94 name: SchemaValue<Option<String>>,
95 aaguid: SchemaValue<Option<String>>,
96 mut extras: FieldMap,
97 create: bool,
98 bind: impl Fn(&UserFieldConfig, Value) -> AuthResult<Value>,
99 ) -> AuthResult<PasskeyFieldPatch> {
100 let mut core = FieldMap::new();
101 for (name, value) in [("name", name), ("aaguid", aaguid)] {
102 let _ = extras.shift_remove(name);
103 if !value.is_undefined() {
104 let _ = core.insert(name.into(), value.into_field_value());
105 }
106 }
107 let config = self.fields(EntityRole::Passkey);
108 let mut fields = config
109 .organization_storage_fields_with_binding(core, extras, create, |_, field, value| {
110 bind(field, value)
111 })
112 .await?;
113 let name_column = storage_name(config, "name");
114 let aaguid_column = storage_name(config, "aaguid");
115 let name = optional_string(&mut fields, name_column);
116 let aaguid = if name_column == aaguid_column {
117 name.clone()
118 } else {
119 optional_string(&mut fields, aaguid_column)
120 };
121 Ok(PasskeyFieldPatch {
122 name,
123 aaguid,
124 additional_fields: fields,
125 })
126 }
127
128 pub async fn project_passkeys(&self, rows: Vec<Passkey>) -> AuthResult<Vec<Passkey>> {
130 let fields = self.fields(EntityRole::Passkey);
131 let records = rows
132 .iter()
133 .map(|row| {
134 let mut storage = row.additional_fields.clone();
135 for (name, value) in [("name", &row.name), ("aaguid", &row.aaguid)] {
136 if !value.is_undefined() {
137 let _ =
138 storage.insert(storage_name(fields, name).into(), value.field_value());
139 }
140 }
141 AdapterRecord::new(FieldMap::new(), storage)
142 })
143 .collect();
144 self.project_passkey_records(
145 rows,
146 records,
147 crate::user_fields::FieldOutputCapabilities::json_only(true),
148 true,
149 )
150 .await
151 }
152
153 pub async fn project_passkey_records(
155 &self,
156 mut rows: Vec<Passkey>,
157 records: Vec<AdapterRecord>,
158 capabilities: crate::user_fields::FieldOutputCapabilities,
159 supports_native_dates: bool,
160 ) -> AuthResult<Vec<Passkey>> {
161 let fields = self.fields(EntityRole::Passkey);
162 let output = fields
163 .project_adapter_records_with_capabilities(records, capabilities, supports_native_dates)
164 .await?;
165 for (row, mut output) in rows.iter_mut().zip(output) {
166 for (name, value) in [("name", &mut row.name), ("aaguid", &mut row.aaguid)] {
167 if fields.fields().contains_key(name) {
168 *value = output
169 .shift_remove(name)
170 .map(SchemaValue::from_field)
171 .unwrap_or_default();
172 }
173 }
174 row.additional_fields = output;
175 }
176 Ok(rows)
177 }
178}