better_auth_api/plugins/user_management/
mod.rs1use async_trait::async_trait;
2use chrono::Duration;
3use std::sync::Arc;
4
5use better_auth_core::entity::AuthUser;
6use better_auth_core::wire::{SessionView, UserView};
7use better_auth_core::{AuthContext, AuthPlugin, AuthRoute};
8use better_auth_core::{AuthError, AuthResult};
9use better_auth_core::{AuthRequest, AuthResponse, HttpMethod};
10
11pub(super) mod handlers;
12pub(super) mod types;
13
14#[cfg(test)]
15mod tests;
16
17use handlers::*;
18use types::*;
19
20#[derive(Debug, Clone)]
29pub struct UserInfo {
30 pub id: String,
31 pub email: Option<String>,
32 pub name: Option<String>,
33 pub email_verified: bool,
34}
35
36impl UserInfo {
37 fn from_auth_user(user: &impl AuthUser) -> Self {
39 Self {
40 id: user.id().to_string(),
41 email: user.email().map(|s| s.to_string()),
42 name: user.name().map(|s| s.to_string()),
43 email_verified: user.email_verified(),
44 }
45 }
46}
47
48#[async_trait]
58pub trait SendChangeEmailConfirmation: Send + Sync {
59 async fn send(
60 &self,
61 user: &UserInfo,
62 new_email: &str,
63 url: &str,
64 token: &str,
65 ) -> AuthResult<()>;
66}
67
68#[async_trait]
73pub trait BeforeDeleteUser: Send + Sync {
74 async fn before_delete(&self, user: &UserInfo) -> AuthResult<()>;
75}
76
77#[async_trait]
79pub trait AfterDeleteUser: Send + Sync {
80 async fn after_delete(&self, user: &UserInfo) -> AuthResult<()>;
81}
82
83#[derive(Clone, Default)]
89pub struct ChangeEmailConfig {
90 pub enabled: bool,
92 pub update_without_verification: bool,
95 pub send_change_email_confirmation: Option<Arc<dyn SendChangeEmailConfirmation>>,
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 require_verification: bool,
125 pub before_delete: Option<Arc<dyn BeforeDeleteUser>>,
127 pub after_delete: Option<Arc<dyn AfterDeleteUser>>,
129}
130
131impl std::fmt::Debug for DeleteUserConfig {
132 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133 f.debug_struct("DeleteUserConfig")
134 .field("enabled", &self.enabled)
135 .field("delete_token_expires_in", &self.delete_token_expires_in)
136 .field("require_verification", &self.require_verification)
137 .field("before_delete", &self.before_delete.is_some())
138 .field("after_delete", &self.after_delete.is_some())
139 .finish()
140 }
141}
142
143impl Default for DeleteUserConfig {
144 fn default() -> Self {
145 Self {
146 enabled: false,
147 delete_token_expires_in: Duration::hours(24),
148 require_verification: true,
149 before_delete: None,
150 after_delete: None,
151 }
152 }
153}
154
155#[derive(Debug, Clone, Default)]
157pub struct UserManagementConfig {
158 pub change_email: ChangeEmailConfig,
159 pub delete_user: DeleteUserConfig,
160}
161
162pub struct UserManagementPlugin {
168 config: UserManagementConfig,
169}
170
171impl UserManagementPlugin {
172 pub fn new() -> Self {
173 Self {
174 config: UserManagementConfig::default(),
175 }
176 }
177
178 pub fn with_config(config: UserManagementConfig) -> Self {
179 Self { config }
180 }
181
182 pub fn change_email_enabled(mut self, enabled: bool) -> Self {
185 self.config.change_email.enabled = enabled;
186 self
187 }
188
189 pub fn update_without_verification(mut self, flag: bool) -> Self {
190 self.config.change_email.update_without_verification = flag;
191 self
192 }
193
194 pub fn send_change_email_confirmation(
195 mut self,
196 cb: Arc<dyn SendChangeEmailConfirmation>,
197 ) -> Self {
198 self.config.change_email.send_change_email_confirmation = Some(cb);
199 self
200 }
201
202 pub fn delete_user_enabled(mut self, enabled: bool) -> Self {
203 self.config.delete_user.enabled = enabled;
204 self
205 }
206
207 pub fn delete_token_expires_in(mut self, duration: Duration) -> Self {
208 self.config.delete_user.delete_token_expires_in = duration;
209 self
210 }
211
212 pub fn require_delete_verification(mut self, require: bool) -> Self {
213 self.config.delete_user.require_verification = require;
214 self
215 }
216
217 pub fn before_delete(mut self, hook: Arc<dyn BeforeDeleteUser>) -> Self {
218 self.config.delete_user.before_delete = Some(hook);
219 self
220 }
221
222 pub fn after_delete(mut self, hook: Arc<dyn AfterDeleteUser>) -> Self {
223 self.config.delete_user.after_delete = Some(hook);
224 self
225 }
226}
227
228impl Default for UserManagementPlugin {
229 fn default() -> Self {
230 Self::new()
231 }
232}
233
234fn append_clear_session_cookies(
235 response: &mut AuthResponse,
236 config: &better_auth_core::AuthConfig,
237) {
238 response.headers.append(
239 "Set-Cookie",
240 better_auth_core::utils::cookie_utils::create_clear_session_cookie(config),
241 );
242 response.headers.append(
243 "Set-Cookie",
244 better_auth_core::utils::cookie_utils::create_clear_cookie(
245 &related_cookie_name(config, "session_data"),
246 config,
247 ),
248 );
249 response.headers.append(
250 "Set-Cookie",
251 better_auth_core::utils::cookie_utils::create_clear_cookie(
252 &related_cookie_name(config, "dont_remember"),
253 config,
254 ),
255 );
256 if config.account.store_account_cookie {
257 response.headers.append(
258 "Set-Cookie",
259 better_auth_core::utils::cookie_utils::create_clear_cookie(
260 &related_cookie_name(config, "account_data"),
261 config,
262 ),
263 );
264 }
265}
266
267fn related_cookie_name(config: &better_auth_core::AuthConfig, suffix: &str) -> String {
268 config
269 .session
270 .cookie_name
271 .strip_suffix("session_token")
272 .map(|prefix| format!("{prefix}{suffix}"))
273 .unwrap_or_else(|| format!("better-auth.{suffix}"))
274}
275
276impl UserManagementPlugin {
281 async fn handle_change_email(
283 &self,
284 req: &AuthRequest,
285 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
286 ) -> AuthResult<AuthResponse> {
287 let (user, _session) = ctx.require_session(req).await?;
288 let user = UserView::from(&user);
289 let body: ChangeEmailRequest = match better_auth_core::validate_request_body(req) {
290 Ok(v) => v,
291 Err(resp) => return Ok(resp),
292 };
293 let response = change_email_core(&body, &user, &self.config, ctx).await?;
294 Ok(AuthResponse::json(200, &response)?)
295 }
296
297 async fn handle_delete_user(
299 &self,
300 req: &AuthRequest,
301 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
302 ) -> AuthResult<AuthResponse> {
303 let (user, session) = ctx.require_session(req).await?;
304 let user = UserView::from(&user);
305 let session = SessionView::from(&session);
306 let body: DeleteUserRequest = match better_auth_core::validate_request_body(req) {
307 Ok(v) => v,
308 Err(resp) => return Ok(resp),
309 };
310 let response = delete_user_core(&body, &user, &session, &self.config, ctx).await?;
311 let mut response = AuthResponse::json(200, &response)?;
312 append_clear_session_cookies(&mut response, &ctx.config);
313 Ok(response)
314 }
315
316 async fn handle_delete_user_callback(
318 &self,
319 req: &AuthRequest,
320 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
321 ) -> AuthResult<AuthResponse> {
322 let (user, _) = ctx
323 .require_session(req)
324 .await
325 .map_err(|_| AuthError::not_found("Failed to get user info"))?;
326 let user = UserView::from(&user);
327 let query: TokenQuery = serde_json::from_value(serde_json::json!({
328 "token": req.query.get("token").cloned(),
329 "callbackURL": req.query.get("callbackURL").cloned(),
330 }))
331 .map_err(|_| AuthError::bad_request("Verification token is required"))?;
332 let response = delete_user_callback_core(&query.token, &user, &self.config, ctx).await?;
333 if let Some(callback_url) = query.callback_url {
334 let mut headers = better_auth_core::Headers::new();
335 _ = headers.insert("Location".to_string(), callback_url);
336 let mut response = AuthResponse {
337 status: 302,
338 headers,
339 body: Vec::new(),
340 };
341 append_clear_session_cookies(&mut response, &ctx.config);
342 return Ok(response);
343 }
344
345 let mut response = AuthResponse::json(200, &response)?;
346 append_clear_session_cookies(&mut response, &ctx.config);
347 Ok(response)
348 }
349}
350
351#[async_trait]
356impl<S: better_auth_core::AuthSchema> AuthPlugin<S> for UserManagementPlugin {
357 fn name(&self) -> &'static str {
358 "user-management"
359 }
360
361 fn routes(&self) -> Vec<AuthRoute> {
362 let mut routes = Vec::new();
363 if self.config.change_email.enabled {
364 routes.push(AuthRoute::post("/change-email", "change_email"));
365 }
366 if self.config.delete_user.enabled {
367 routes.push(AuthRoute::post("/delete-user", "delete_user"));
368 routes.push(AuthRoute::get(
369 "/delete-user/callback",
370 "delete_user_callback",
371 ));
372 }
373 routes
374 }
375
376 async fn on_request(
377 &self,
378 req: &AuthRequest,
379 ctx: &AuthContext<S>,
380 ) -> AuthResult<Option<AuthResponse>> {
381 match (req.method(), req.path()) {
382 (HttpMethod::Post, "/change-email") if self.config.change_email.enabled => {
384 Ok(Some(self.handle_change_email(req, ctx).await?))
385 }
386 (HttpMethod::Post, "/delete-user") if self.config.delete_user.enabled => {
388 Ok(Some(self.handle_delete_user(req, ctx).await?))
389 }
390 (HttpMethod::Get, "/delete-user/callback") if self.config.delete_user.enabled => {
391 Ok(Some(self.handle_delete_user_callback(req, ctx).await?))
392 }
393 _ => Ok(None),
394 }
395 }
396}