use better_auth_core::{AuthError, AuthResult};
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value, json};
use std::{future::Future, pin::Pin, sync::Arc};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DeviceRequestIssue {
pub message: String,
#[serde(default)]
pub path: Vec<Value>,
#[serde(default, flatten)]
pub details: Map<String, Value>,
}
impl DeviceRequestIssue {
pub(super) fn invalid_type(field: Option<&str>, expected: &str, value: Option<&Value>) -> Self {
Self {
message: format!(
"Invalid input: expected {expected}, received {}",
crate::plugins::json_body::type_name(value)
),
path: field.into_iter().map(|field| json!(field)).collect(),
details: Map::from_iter([
("code".into(), json!("invalid_type")),
("expected".into(), json!(expected)),
]),
}
}
pub(super) fn message(&self) -> String {
let mut path = String::from("body");
for segment in &self.path {
let key = segment.get("key").unwrap_or(segment);
path.push('.');
match key {
Value::String(key) => path.push_str(key),
key => path.push_str(&key.to_string()),
}
}
format!("[{path}] {}", self.message)
}
}
pub enum DeviceFieldValidation {
Value(Option<Value>),
Issues(Vec<DeviceRequestIssue>),
}
type FieldResult = AuthResult<DeviceFieldValidation>;
type FieldFuture = Pin<Box<dyn Future<Output = FieldResult> + Send>>;
#[derive(Clone)]
pub struct DeviceRequestField {
callback: Callback,
openapi: Option<RequestFieldSchema>,
}
#[derive(Clone)]
struct RequestFieldSchema {
schema: Value,
required: bool,
}
#[derive(Clone)]
enum Callback {
Sync(Arc<dyn Fn(Option<Value>) -> FieldResult + Send + Sync>),
Async(Arc<dyn Fn(Option<Value>) -> FieldFuture + Send + Sync>),
}
impl DeviceRequestField {
pub fn new(callback: impl Fn(Option<Value>) -> FieldResult + Send + Sync + 'static) -> Self {
Self {
callback: Callback::Sync(Arc::new(callback)),
openapi: None,
}
}
pub fn new_async<F, Fut>(callback: F) -> Self
where
F: Fn(Option<Value>) -> Fut + Send + Sync + 'static,
Fut: Future<Output = FieldResult> + Send + 'static,
{
Self {
callback: Callback::Async(Arc::new(move |value| Box::pin(callback(value)))),
openapi: None,
}
}
pub fn openapi_schema(mut self, schema: Value, required: bool) -> Self {
self.openapi = Some(RequestFieldSchema { schema, required });
self
}
pub(super) async fn validate(&self, value: Option<Value>) -> FieldResult {
match &self.callback {
Callback::Sync(callback) => callback(value),
Callback::Async(callback) => callback(value).await,
}
}
}
type ValidationErrorCallback = dyn Fn(&[DeviceRequestIssue]) -> AuthResult<()> + Send + Sync;
#[derive(Clone, Default)]
pub struct DeviceRequestFields {
pub(super) fields: IndexMap<String, DeviceRequestField>,
on_validation_error: Option<Arc<ValidationErrorCallback>>,
}
impl DeviceRequestFields {
pub fn new() -> Self {
Self::default()
}
pub fn field(mut self, name: impl Into<String>, field: DeviceRequestField) -> AuthResult<Self> {
let name = name.into();
if matches!(name.as_str(), "client_id" | "user_id" | "scope") {
return Err(AuthError::config(format!(
"Device authorization grant request fields must be additional and cannot redefine request fields: {name}",
)));
}
let _ = self.fields.insert(name, field);
Ok(self)
}
pub fn on_validation_error(
mut self,
callback: impl Fn(&[DeviceRequestIssue]) -> AuthResult<()> + Send + Sync + 'static,
) -> Self {
self.on_validation_error = Some(Arc::new(callback));
self
}
pub(super) fn report(&self, issues: &[DeviceRequestIssue]) -> AuthResult<()> {
if let Some(callback) = &self.on_validation_error {
callback(issues)?;
}
Ok(())
}
pub(super) fn openapi(&self) -> (Map<String, Value>, Vec<String>) {
let mut properties = Map::new();
let mut required = Vec::new();
for (name, field) in &self.fields {
if let Some(metadata) = &field.openapi {
let _ = properties.insert(name.clone(), metadata.schema.clone());
if metadata.required {
required.push(name.clone());
}
}
}
(properties, required)
}
}