use super::{TwoFactorConfig, TwoFactorPlugin, request};
use better_auth_core::{
AuthContext, AuthError, AuthResponse, AuthResult, AuthRoute, AuthSchema, FieldMap, FieldValue,
HttpMethod, NativeRequest,
};
use serde_json::Value;
pub struct TwoFactorApi<'a, S: AuthSchema> {
plugin: TwoFactorPlugin,
context: &'a AuthContext<S>,
source: NativeRequest<'a>,
}
impl<'a, S: AuthSchema> TwoFactorApi<'a, S> {
pub fn from_context(context: &'a AuthContext<S>) -> AuthResult<Self> {
let config = context
.extensions
.get::<TwoFactorConfig>()
.ok_or_else(|| AuthError::config("TwoFactorPlugin is not registered"))?;
Ok(Self {
plugin: TwoFactorPlugin::with_config(config.clone()),
context,
source: NativeRequest::default(),
})
}
pub fn with_request(mut self, source: NativeRequest<'a>) -> Self {
self.source = source;
self
}
pub async fn generate_totp(&self, body: Option<Value>) -> AuthResult<String> {
let route = AuthRoute::server_only(HttpMethod::Post, "generateTOTP")
.body_validator(|request| request::validate_native(request, "generateTOTP"));
let response = self
.context
.dispatch_native(self.source, route, body, None, |request, _| async move {
let body = request
.validated_body::<request::GenerateTotpRequest>()
.ok_or_else(|| AuthError::internal("Missing validated TOTP input"))?;
AuthResponse::json(
None,
&serde_json::json!({"code":self.plugin.generate_totp(&body.secret)?}),
)
})
.await?;
#[derive(serde::Deserialize)]
struct Result {
code: String,
}
Ok(serde_json::from_slice::<Result>(&response.body.bytes()?)?.code)
}
pub async fn view_backup_codes(&self, body: Option<Value>) -> AuthResult<FieldValue> {
let route = AuthRoute::server_only(HttpMethod::Post, "viewBackupCodes")
.body_validator(|request| request::validate_native(request, "viewBackupCodes"));
let response = self
.context
.dispatch_native(
self.source,
route,
body,
None,
|request, context| async move {
let body = request
.validated_body::<request::ViewBackupCodesRequest>()
.ok_or_else(|| {
AuthError::internal("Missing validated backup code input")
})?;
let codes = self
.plugin
.view_backup_codes(&body.user_id, &context)
.await?;
Ok(AuthResponse::native(
None,
FieldMap::from([
("status".into(), true.into()),
("backupCodes".into(), codes),
])
.into(),
))
},
)
.await?;
Ok(response
.body
.field_value()?
.as_object()
.map(|fields| fields.get("backupCodes"))
.transpose()?
.flatten()
.unwrap_or_default())
}
}