Skip to main content

better_auth_api/plugins/
endpoint_context.rs

1//! Typed endpoint context for application callbacks.
2
3use better_auth_core::{
4    AuthContext, AuthInitContext, AuthPlugin, AuthRequest, AuthResponse, AuthResult, AuthRoute,
5    AuthSchema, BeforeRequestAction, FieldValue,
6};
7mod owned;
8pub use owned::OwnedEndpointContext;
9
10use serde_json::Value;
11use std::sync::Arc;
12
13/// The parsed endpoint input and the active authentication runtime.
14pub struct EndpointContext<'a, S: AuthSchema> {
15    input_request: Option<&'a AuthRequest>,
16    /// Original Request supplied by HTTP transport or an explicit native caller.
17    pub request: Option<&'a AuthRequest>,
18    /// Endpoint path, including synthetic dispatch from another endpoint.
19    pub path: Option<&'a str>,
20    /// Values captured from the matched endpoint template.
21    pub params: std::collections::HashMap<String, String>,
22    /// Input after endpoint validation and unknown-field filtering.
23    pub body: FieldValue,
24    /// Full typed runtime, including the store, options, metadata, and extensions.
25    pub auth: &'a AuthContext<S>,
26    /// Active transaction. Use this store for database work inside transactional callbacks.
27    pub transaction: Option<&'a dyn better_auth_core::store::AuthTransaction<S>>,
28    /// Session already resolved by this endpoint before the callback.
29    pub session: Option<(
30        better_auth_core::wire::UserView,
31        better_auth_core::wire::SessionView,
32    )>,
33    /// Completed response when the callback runs from an after hook.
34    pub response: Option<&'a AuthResponse>,
35}
36impl<'a, S: AuthSchema> EndpointContext<'a, S> {
37    /// Access installed test helpers using this endpoint's active transaction.
38    pub fn test(&'a self) -> AuthResult<super::test_utils::TestUtilsApi<'a, S>> {
39        super::test_utils::TestUtilsApi::from_endpoint(self)
40    }
41
42    /// Supplied endpoint headers, preserving omission for native calls.
43    pub fn headers(&self) -> Option<&std::collections::HashMap<String, String>> {
44        self.input_request.and_then(AuthRequest::endpoint_headers)
45    }
46
47    /// Endpoint query, preserving native omission separately from null and an empty object.
48    pub fn query(&self) -> Option<&Value> {
49        self.input_request
50            .and_then(|request| request.query.as_ref())
51    }
52
53    /// Active endpoint input and shared server state, independent of the original Request.
54    pub fn input_request(&self) -> Option<&'a AuthRequest> {
55        self.input_request
56    }
57
58    /// Set a response header without replacing other header names.
59    pub fn set_header(&self, name: &str, value: impl Into<String>) -> AuthResult<()> {
60        self.input_request
61            .ok_or_else(|| {
62                better_auth_core::AuthError::internal("Endpoint response is unavailable")
63            })?
64            .set_response_header(name, value.into())
65    }
66
67    /// Append a response header, including another `Set-Cookie` value.
68    pub fn append_header(&self, name: &str, value: impl Into<String>) -> AuthResult<()> {
69        self.input_request
70            .ok_or_else(|| {
71                better_auth_core::AuthError::internal("Endpoint response is unavailable")
72            })?
73            .append_response_header(name, value.into())
74    }
75
76    /// Identity supplied to the most recent session-cookie write in this endpoint.
77    pub fn new_session(&self) -> AuthResult<Option<better_auth_core::session::NativeSessionData>> {
78        self.input_request
79            .map(AuthRequest::new_session)
80            .transpose()
81            .map(Option::flatten)
82    }
83    /// Use the registered Email OTP plugin within this endpoint's active transaction.
84    pub fn email_otp(&self) -> AuthResult<super::email_otp::EmailOtpApi<'a, S>> {
85        super::email_otp::EmailOtpApi::from_endpoint(self)
86    }
87
88    /// Add organization members within this endpoint's active transaction.
89    pub fn organization(&self) -> AuthResult<super::organization::OrganizationApi<'a, S>> {
90        super::organization::OrganizationApi::from_endpoint(self)
91    }
92
93    /// Consume a phone OTP within this endpoint's active transaction.
94    pub fn phone_number(&self) -> AuthResult<super::phone_number::PhoneNumberApi<'a, S>> {
95        super::phone_number::PhoneNumberApi::from_endpoint(self)
96    }
97
98    /// Use the registered JWT key adapter within this endpoint's active transaction.
99    pub fn jwt(&self) -> AuthResult<super::jwt::JwtApi<'a, S>> {
100        super::jwt::JwtApi::from_endpoint(self)
101    }
102
103    /// Construct a native endpoint without inferring a Request from ambient HTTP activity.
104    pub fn native(
105        input_request: Option<&'a AuthRequest>,
106        original_request: Option<&'a AuthRequest>,
107        body: FieldValue,
108        auth: &'a AuthContext<S>,
109    ) -> Self {
110        let mut context = Self::new(input_request, body, auth);
111        context.request = original_request;
112        context.params.clear();
113        context
114    }
115
116    /// Construct an endpoint context from its request, validated body, and runtime.
117    /// Set `transaction` when the caller executes inside an active database transaction.
118    pub fn new(
119        request: Option<&'a AuthRequest>,
120        body: FieldValue,
121        auth: &'a AuthContext<S>,
122    ) -> Self {
123        Self {
124            input_request: request,
125            request: request.and_then(|request| {
126                request.original_request().or_else(|| {
127                    better_auth_core::hooks::current_request_hook_context()
128                        .is_none_or(|context| context.is_http)
129                        .then_some(request)
130                })
131            }),
132            path: request.map(AuthRequest::path),
133            params: better_auth_core::hooks::current_request_hook_context()
134                .map(|context| context.params)
135                .unwrap_or_default(),
136            body,
137            auth,
138            transaction: None,
139            session: None,
140            response: None,
141        }
142    }
143}
144
145pub(crate) struct WithCallbacks<P, C> {
146    pub(crate) plugin: P,
147    pub(crate) callbacks: Arc<C>,
148}
149
150#[async_trait::async_trait]
151impl<S: AuthSchema, P: AuthPlugin<S>, C: Send + Sync + 'static> AuthPlugin<S>
152    for WithCallbacks<P, C>
153{
154    fn name(&self) -> &'static str {
155        self.plugin.name()
156    }
157    fn telemetry_plugin_id(&self) -> Option<&'static str> {
158        self.plugin.telemetry_plugin_id()
159    }
160    fn routes(&self) -> Vec<AuthRoute> {
161        self.plugin.routes()
162    }
163    fn openapi(&self) -> AuthResult<better_auth_core::openapi::OpenApiPluginMetadata> {
164        self.plugin.openapi()
165    }
166    fn password_hasher(&self) -> Option<Arc<dyn better_auth_core::PasswordHasher>> {
167        self.plugin.password_hasher()
168    }
169    fn telemetry(&self, options: &mut better_auth_core::observability::telemetry::PluginTelemetry) {
170        self.plugin.telemetry(options);
171        let callbacks = self.callbacks.as_ref() as &dyn std::any::Any;
172        if callbacks.is::<super::email_verification::EmailVerificationCallbacks<S>>() {
173            options.email_verification.send_verification_email = true;
174        }
175        if callbacks.is::<super::password_management::PasswordManagementCallbacks<S>>() {
176            options.email_and_password.send_reset_password = true;
177        }
178        if let Some(callbacks) =
179            callbacks.downcast_ref::<super::user_management::UserManagementCallbacks<S>>()
180        {
181            options.send_change_email_confirmation |= callbacks.has_confirmation_sender();
182        }
183        if let Some(callbacks) = callbacks.downcast_ref::<super::oauth::OAuthCallbacks<S>>() {
184            for provider in &mut options.social_providers {
185                provider.verify_id_token |= callbacks.verifiers.contains_key(&provider.id);
186            }
187        }
188    }
189    fn rate_limits(&self) -> AuthResult<Vec<better_auth_core::middleware::PluginRateLimit>> {
190        self.plugin.rate_limits()
191    }
192    async fn on_init(&self, ctx: &mut AuthInitContext<S>) -> AuthResult<()> {
193        ctx.extensions.insert(self.callbacks.clone());
194        self.plugin.on_init(ctx).await
195    }
196    async fn before_request(
197        &self,
198        req: &AuthRequest,
199        ctx: &AuthContext<S>,
200    ) -> AuthResult<Option<BeforeRequestAction>> {
201        self.plugin.before_request(req, ctx).await
202    }
203    async fn on_http_request(
204        &self,
205        req: &AuthRequest,
206        ctx: &AuthContext<S>,
207    ) -> AuthResult<Option<AuthResponse>> {
208        self.plugin.on_http_request(req, ctx).await
209    }
210    async fn on_http_response(
211        &self,
212        req: &AuthRequest,
213        response: &mut AuthResponse,
214        ctx: &AuthContext<S>,
215    ) -> AuthResult<Option<AuthResponse>> {
216        self.plugin.on_http_response(req, response, ctx).await
217    }
218    async fn on_request(
219        &self,
220        req: &AuthRequest,
221        ctx: &AuthContext<S>,
222    ) -> AuthResult<Option<AuthResponse>> {
223        self.plugin.on_request(req, ctx).await
224    }
225    async fn after_request(
226        &self,
227        req: &AuthRequest,
228        response: &mut AuthResponse,
229        ctx: &AuthContext<S>,
230    ) -> AuthResult<()> {
231        self.plugin.after_request(req, response, ctx).await
232    }
233}
234
235#[cfg(test)]
236mod tests;