Skip to main content

better_auth_api/plugins/device_authorization/
request_fields.rs

1use better_auth_core::{AuthError, AuthResult};
2use indexmap::IndexMap;
3use serde::{Deserialize, Serialize};
4use serde_json::{Map, Value, json};
5use std::{future::Future, pin::Pin, sync::Arc};
6
7/// One request-schema issue. Paths are relative to the field being validated.
8#[derive(Clone, Debug, Serialize, Deserialize)]
9pub struct DeviceRequestIssue {
10    /// Human-readable validation failure.
11    pub message: String,
12    /// Standard Schema path segments, including JSON-visible `{ "key": ... }` segments.
13    #[serde(default)]
14    pub path: Vec<Value>,
15    /// Validator-specific details, such as Zod's `code` and `expected` fields.
16    #[serde(default, flatten)]
17    pub details: Map<String, Value>,
18}
19
20impl DeviceRequestIssue {
21    pub(super) fn invalid_type(field: Option<&str>, expected: &str, value: Option<&Value>) -> Self {
22        Self {
23            message: format!(
24                "Invalid input: expected {expected}, received {}",
25                crate::plugins::json_body::type_name(value)
26            ),
27            path: field.into_iter().map(|field| json!(field)).collect(),
28            details: Map::from_iter([
29                ("code".into(), json!("invalid_type")),
30                ("expected".into(), json!(expected)),
31            ]),
32        }
33    }
34
35    pub(super) fn message(&self) -> String {
36        let mut path = String::from("body");
37        for segment in &self.path {
38            let key = segment.get("key").unwrap_or(segment);
39            path.push('.');
40            match key {
41                Value::String(key) => path.push_str(key),
42                key => path.push_str(&key.to_string()),
43            }
44        }
45        format!("[{path}] {}", self.message)
46    }
47}
48
49/// A parsed field value or the field's structured validation issues.
50pub enum DeviceFieldValidation {
51    /// `None` preserves omission; `Some(Value::Null)` preserves an explicit null.
52    Value(Option<Value>),
53    /// Mark validation as failed, including when the issue list is empty.
54    Issues(Vec<DeviceRequestIssue>),
55}
56
57type FieldResult = AuthResult<DeviceFieldValidation>;
58type FieldFuture = Pin<Box<dyn Future<Output = FieldResult> + Send>>;
59
60/// Validate one declared additional request field without changing native request fields.
61#[derive(Clone)]
62pub struct DeviceRequestField {
63    callback: Callback,
64    openapi: Option<RequestFieldSchema>,
65}
66
67#[derive(Clone)]
68struct RequestFieldSchema {
69    schema: Value,
70    required: bool,
71}
72
73#[derive(Clone)]
74enum Callback {
75    Sync(Arc<dyn Fn(Option<Value>) -> FieldResult + Send + Sync>),
76    Async(Arc<dyn Fn(Option<Value>) -> FieldFuture + Send + Sync>),
77}
78
79impl DeviceRequestField {
80    /// Use a synchronous field schema.
81    pub fn new(callback: impl Fn(Option<Value>) -> FieldResult + Send + Sync + 'static) -> Self {
82        Self {
83            callback: Callback::Sync(Arc::new(callback)),
84            openapi: None,
85        }
86    }
87
88    /// Await a field schema once, without emulating JavaScript's async-detection probe.
89    pub fn new_async<F, Fut>(callback: F) -> Self
90    where
91        F: Fn(Option<Value>) -> Fut + Send + Sync + 'static,
92        Fut: Future<Output = FieldResult> + Send + 'static,
93    {
94        Self {
95            callback: Callback::Async(Arc::new(move |value| Box::pin(callback(value)))),
96            openapi: None,
97        }
98    }
99
100    /// Document the input schema and input requiredness without executing the validator.
101    ///
102    /// Defaults and transformations do not determine input requiredness. This declaration
103    /// changes documentation only; the callback remains responsible for validation.
104    pub fn openapi_schema(mut self, schema: Value, required: bool) -> Self {
105        self.openapi = Some(RequestFieldSchema { schema, required });
106        self
107    }
108
109    pub(super) async fn validate(&self, value: Option<Value>) -> FieldResult {
110        match &self.callback {
111            Callback::Sync(callback) => callback(value),
112            Callback::Async(callback) => callback(value).await,
113        }
114    }
115}
116
117type ValidationErrorCallback = dyn Fn(&[DeviceRequestIssue]) -> AuthResult<()> + Send + Sync;
118
119/// Additional Device request fields. Storage policies remain separate in `ModelFields`.
120#[derive(Clone, Default)]
121pub struct DeviceRequestFields {
122    pub(super) fields: IndexMap<String, DeviceRequestField>,
123    on_validation_error: Option<Arc<ValidationErrorCallback>>,
124}
125
126impl DeviceRequestFields {
127    /// Start with no additional request fields.
128    pub fn new() -> Self {
129        Self::default()
130    }
131
132    /// Declare a field in validation order. Native request fields cannot be replaced.
133    pub fn field(mut self, name: impl Into<String>, field: DeviceRequestField) -> AuthResult<Self> {
134        let name = name.into();
135        if matches!(name.as_str(), "client_id" | "user_id" | "scope") {
136            return Err(AuthError::config(format!(
137                "Device authorization grant request fields must be additional and cannot redefine request fields: {name}",
138            )));
139        }
140        let _ = self.fields.insert(name, field);
141        Ok(self)
142    }
143
144    /// Observe issues or return an application error before the default OAuth error is produced.
145    pub fn on_validation_error(
146        mut self,
147        callback: impl Fn(&[DeviceRequestIssue]) -> AuthResult<()> + Send + Sync + 'static,
148    ) -> Self {
149        self.on_validation_error = Some(Arc::new(callback));
150        self
151    }
152
153    pub(super) fn report(&self, issues: &[DeviceRequestIssue]) -> AuthResult<()> {
154        if let Some(callback) = &self.on_validation_error {
155            callback(issues)?;
156        }
157        Ok(())
158    }
159
160    pub(super) fn openapi(&self) -> (Map<String, Value>, Vec<String>) {
161        let mut properties = Map::new();
162        let mut required = Vec::new();
163        for (name, field) in &self.fields {
164            if let Some(metadata) = &field.openapi {
165                let _ = properties.insert(name.clone(), metadata.schema.clone());
166                if metadata.required {
167                    required.push(name.clone());
168                }
169            }
170        }
171        (properties, required)
172    }
173}