better_auth_api/plugins/user_management/
mod.rs1use async_trait::async_trait;
2use chrono::Duration;
3use std::sync::Arc;
4
5use better_auth_core::wire::UserView;
6use better_auth_core::{AuthContext, AuthPlugin, AuthRoute};
7use better_auth_core::{AuthError, AuthResult};
8use better_auth_core::{AuthRequest, AuthResponse, HttpMethod};
9
10mod callbacks;
11pub(super) mod handlers;
12pub use callbacks::{ChangeEmailConfirmation, UserManagementCallbacks};
13mod request;
14pub(super) mod types;
15
16#[cfg(test)]
17mod tests;
18
19use handlers::*;
20use types::*;
21
22pub type UserInfo = UserView;
28
29#[async_trait]
38pub trait SendChangeEmailConfirmation: Send + Sync {
39 async fn send(
40 &self,
41 user: &UserInfo,
42 new_email: &str,
43 url: &str,
44 token: &str,
45 ) -> AuthResult<()>;
46}
47
48#[async_trait]
53pub trait BeforeDeleteUser: Send + Sync {
54 async fn before_delete(&self, user: &UserView, request: Option<&AuthRequest>)
55 -> AuthResult<()>;
56}
57
58#[async_trait]
60pub trait AfterDeleteUser: Send + Sync {
61 async fn after_delete(&self, user: &UserView, request: Option<&AuthRequest>) -> AuthResult<()>;
62}
63
64#[async_trait]
66pub trait SendDeleteAccountVerification: Send + Sync {
67 async fn send(
68 &self,
69 user: &UserView,
70 url: &str,
71 token: &str,
72 request: Option<&AuthRequest>,
73 ) -> AuthResult<()>;
74}
75
76#[derive(Clone, Default)]
82pub struct ChangeEmailConfig {
83 pub enabled: Option<bool>,
85 pub update_without_verification: bool,
88 pub send_change_email_confirmation: Option<Arc<dyn SendChangeEmailConfirmation>>,
90}
91
92impl ChangeEmailConfig {
93 pub fn enabled(&self) -> bool {
95 self.enabled.unwrap_or(false)
96 }
97}
98
99impl std::fmt::Debug for ChangeEmailConfig {
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 f.debug_struct("ChangeEmailConfig")
102 .field("enabled", &self.enabled)
103 .field(
104 "update_without_verification",
105 &self.update_without_verification,
106 )
107 .field(
108 "send_change_email_confirmation",
109 &self.send_change_email_confirmation.is_some(),
110 )
111 .finish()
112 }
113}
114
115#[derive(Clone)]
117pub struct DeleteUserConfig {
118 pub enabled: bool,
120 pub delete_token_expires_in: Duration,
122 pub send_delete_account_verification: Option<Arc<dyn SendDeleteAccountVerification>>,
124 pub before_delete: Option<Arc<dyn BeforeDeleteUser>>,
126 pub after_delete: Option<Arc<dyn AfterDeleteUser>>,
128}
129
130impl std::fmt::Debug for DeleteUserConfig {
131 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132 f.debug_struct("DeleteUserConfig")
133 .field("enabled", &self.enabled)
134 .field("delete_token_expires_in", &self.delete_token_expires_in)
135 .field(
136 "send_delete_account_verification",
137 &self.send_delete_account_verification.is_some(),
138 )
139 .field("before_delete", &self.before_delete.is_some())
140 .field("after_delete", &self.after_delete.is_some())
141 .finish()
142 }
143}
144
145impl Default for DeleteUserConfig {
146 fn default() -> Self {
147 Self {
148 enabled: false,
149 delete_token_expires_in: Duration::hours(24),
150 send_delete_account_verification: None,
151 before_delete: None,
152 after_delete: None,
153 }
154 }
155}
156
157#[derive(Debug, Clone, Default)]
159pub struct UserManagementConfig {
160 pub change_email: ChangeEmailConfig,
161 pub delete_user: DeleteUserConfig,
162}
163
164pub struct UserManagementPlugin {
170 config: UserManagementConfig,
171}
172
173impl UserManagementPlugin {
174 pub fn new() -> Self {
175 Self {
176 config: UserManagementConfig::default(),
177 }
178 }
179
180 pub fn with_config(config: UserManagementConfig) -> Self {
181 Self { config }
182 }
183
184 pub fn change_email_enabled(mut self, enabled: bool) -> Self {
187 self.config.change_email.enabled = Some(enabled);
188 self
189 }
190
191 pub fn update_without_verification(mut self, flag: bool) -> Self {
192 self.config.change_email.update_without_verification = flag;
193 self
194 }
195
196 pub fn send_change_email_confirmation(
197 mut self,
198 cb: Arc<dyn SendChangeEmailConfirmation>,
199 ) -> Self {
200 self.config.change_email.send_change_email_confirmation = Some(cb);
201 self
202 }
203
204 pub fn delete_user_enabled(mut self, enabled: bool) -> Self {
205 self.config.delete_user.enabled = enabled;
206 self
207 }
208
209 pub fn delete_token_expires_in(mut self, duration: Duration) -> Self {
210 self.config.delete_user.delete_token_expires_in = duration;
211 self
212 }
213
214 pub fn send_delete_account_verification(
215 mut self,
216 sender: Arc<dyn SendDeleteAccountVerification>,
217 ) -> Self {
218 self.config.delete_user.send_delete_account_verification = Some(sender);
219 self
220 }
221
222 pub fn before_delete(mut self, hook: Arc<dyn BeforeDeleteUser>) -> Self {
223 self.config.delete_user.before_delete = Some(hook);
224 self
225 }
226
227 pub fn after_delete(mut self, hook: Arc<dyn AfterDeleteUser>) -> Self {
228 self.config.delete_user.after_delete = Some(hook);
229 self
230 }
231}
232
233impl Default for UserManagementPlugin {
234 fn default() -> Self {
235 Self::new()
236 }
237}
238
239impl UserManagementPlugin {
240 async fn handle_change_email(
242 &self,
243 req: &AuthRequest,
244 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
245 ) -> AuthResult<AuthResponse> {
246 let (user, session) = ctx
247 .require_authoritative_session(req)
248 .await
249 .map_err(|error| match error {
250 AuthError::Unauthenticated => AuthError::Upstream {
251 status: 401,
252 code: "UNAUTHORIZED",
253 message: "Unauthorized",
254 },
255 error => error,
256 })?;
257 let body = request::change_email(req)?;
258 let response = change_email_core(&body, &user, &session, req, &self.config, ctx).await?;
259 Ok(AuthResponse::json(200, &response)?)
260 }
261
262 async fn handle_delete_user(
264 &self,
265 req: &AuthRequest,
266 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
267 ) -> AuthResult<AuthResponse> {
268 let (user, session) = ctx.require_session(req).await?;
269 let user = ctx.user_view(&user).await?;
270 let body = request::delete_user(req)?;
271 let response = delete_user_core(&body, &user, &session, req, &self.config, ctx).await?;
272 let mut response = AuthResponse::json(200, &response)?;
273 for (name, value) in req.take_response_headers()? {
274 response.headers.append(name, value);
275 }
276 Ok(response)
277 }
278
279 async fn handle_delete_user_callback(
281 &self,
282 req: &AuthRequest,
283 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
284 ) -> AuthResult<AuthResponse> {
285 let (user, _) = ctx
286 .require_authoritative_session(req)
287 .await
288 .map_err(|error| {
289 if error.status_code() == 401 {
290 AuthError::not_found("Failed to get user info")
291 } else {
292 error
293 }
294 })?;
295 let user = ctx.user_view(&user).await?;
296 let query: TokenQuery = serde_json::from_value(serde_json::json!({
297 "token": req.query_string("token")?.map(str::to_owned),
298 "callbackURL": req.query_string("callbackURL")?.map(str::to_owned),
299 }))
300 .map_err(|_| AuthError::bad_request("Verification token is required"))?;
301 let response =
302 delete_user_callback_core(&query.token, &user, req, &self.config, ctx).await?;
303 if let Some(callback_url) = query.callback_url {
304 let mut headers = better_auth_core::Headers::new();
305 _ = headers.insert("Location".to_string(), callback_url);
306 let mut response = AuthResponse::new(302);
307 response.headers = headers;
308 for (name, value) in req.take_response_headers()? {
309 response.headers.append(name, value);
310 }
311 return Ok(response);
312 }
313
314 let mut response = AuthResponse::json(200, &response)?;
315 for (name, value) in req.take_response_headers()? {
316 response.headers.append(name, value);
317 }
318 Ok(response)
319 }
320}
321
322#[async_trait]
327impl<S: better_auth_core::AuthSchema> AuthPlugin<S> for UserManagementPlugin {
328 fn telemetry(&self, options: &mut better_auth_core::observability::telemetry::PluginTelemetry) {
329 options.change_email_enabled = self.config.change_email.enabled;
330 options.send_change_email_confirmation = self
331 .config
332 .change_email
333 .send_change_email_confirmation
334 .is_some();
335 }
336
337 fn name(&self) -> &'static str {
338 "user-management"
339 }
340
341 fn telemetry_plugin_id(&self) -> Option<&'static str> {
342 None
343 }
344
345 fn routes(&self) -> Vec<AuthRoute> {
346 let mut routes = vec![
347 AuthRoute::post("/change-email", "changeEmail")
348 .body_validator(request::change_email_body),
349 ];
350 if self.config.delete_user.enabled {
351 routes.push(
352 AuthRoute::post("/delete-user", "deleteUser")
353 .body_validator(request::delete_user_body),
354 );
355 routes.push(
356 AuthRoute::get("/delete-user/callback", "deleteUserCallback")
357 .query_validator(crate::plugins::query_input::verify_email),
358 );
359 }
360 routes
361 }
362
363 async fn on_request(
364 &self,
365 req: &AuthRequest,
366 ctx: &AuthContext<S>,
367 ) -> AuthResult<Option<AuthResponse>> {
368 match (req.method(), req.path()) {
369 (HttpMethod::Post, "/change-email") => {
371 Ok(Some(self.handle_change_email(req, ctx).await?))
372 }
373 (HttpMethod::Post, "/delete-user") if self.config.delete_user.enabled => {
375 Ok(Some(self.handle_delete_user(req, ctx).await?))
376 }
377 (HttpMethod::Get, "/delete-user/callback") if self.config.delete_user.enabled => {
378 Ok(Some(self.handle_delete_user_callback(req, ctx).await?))
379 }
380 _ => Ok(None),
381 }
382 }
383}