Skip to main content

better_auth_api/plugins/admin/
mod.rs

1use better_auth_core::adapters::DatabaseAdapter;
2use better_auth_core::entity::AuthUser;
3use better_auth_core::{AuthContext, AuthError, AuthResult};
4use better_auth_core::{AuthRequest, AuthResponse};
5
6use better_auth_core::utils::cookie_utils::create_session_cookie;
7
8#[cfg(feature = "axum")]
9use super::StatusResponse;
10
11pub(super) mod handlers;
12pub(super) mod types;
13
14#[cfg(test)]
15mod tests;
16
17use handlers::*;
18use types::*;
19
20// ---------------------------------------------------------------------------
21// Plugin & config
22// ---------------------------------------------------------------------------
23
24/// Admin plugin for user management operations.
25///
26/// Provides endpoints for creating users, listing users, banning/unbanning,
27/// role management, session management, password management, user impersonation,
28/// and permission checks.
29///
30/// All endpoints require an authenticated session with the `admin` role.
31pub struct AdminPlugin {
32    config: AdminConfig,
33}
34
35/// Configuration for the admin plugin.
36#[derive(Debug, Clone, better_auth_core::PluginConfig)]
37#[plugin(name = "AdminPlugin")]
38pub struct AdminConfig {
39    /// The role required to access admin endpoints (default: `"admin"`).
40    #[config(default = "admin".to_string())]
41    pub admin_role: String,
42    /// Default role assigned to newly created users (default: `"user"`).
43    #[config(default = "user".to_string())]
44    pub default_user_role: String,
45    /// Whether to allow banning other admins (default: `false`).
46    #[config(default = false)]
47    pub allow_ban_admin: bool,
48    /// Default number of users returned in list-users (default: 100).
49    #[config(default = 100)]
50    pub default_page_limit: usize,
51    /// Maximum number of users returned in list-users (default: 500).
52    #[config(default = 500)]
53    pub max_page_limit: usize,
54}
55
56// ---------------------------------------------------------------------------
57// Plugin trait implementation
58// ---------------------------------------------------------------------------
59
60better_auth_core::impl_auth_plugin! {
61    AdminPlugin, "admin";
62    routes {
63        post "/admin/set-role" => handle_set_role, "admin_set_role";
64        post "/admin/create-user" => handle_create_user, "admin_create_user";
65        get  "/admin/list-users" => handle_list_users, "admin_list_users";
66        post "/admin/list-user-sessions" => handle_list_user_sessions, "admin_list_user_sessions";
67        post "/admin/ban-user" => handle_ban_user, "admin_ban_user";
68        post "/admin/unban-user" => handle_unban_user, "admin_unban_user";
69        post "/admin/impersonate-user" => handle_impersonate_user, "admin_impersonate_user";
70        post "/admin/stop-impersonating" => handle_stop_impersonating, "admin_stop_impersonating";
71        post "/admin/revoke-user-session" => handle_revoke_user_session, "admin_revoke_user_session";
72        post "/admin/revoke-user-sessions" => handle_revoke_user_sessions, "admin_revoke_user_sessions";
73        post "/admin/remove-user" => handle_remove_user, "admin_remove_user";
74        post "/admin/set-user-password" => handle_set_user_password, "admin_set_user_password";
75        post "/admin/has-permission" => handle_has_permission, "admin_has_permission";
76    }
77}
78
79// ---------------------------------------------------------------------------
80// Handler implementations (old -- delegate to core)
81// ---------------------------------------------------------------------------
82
83impl AdminPlugin {
84    /// Authenticate the caller and verify they have the admin role.
85    async fn require_admin<DB: DatabaseAdapter>(
86        &self,
87        req: &AuthRequest,
88        ctx: &AuthContext<DB>,
89    ) -> AuthResult<(DB::User, DB::Session)> {
90        let (user, session) = ctx.require_session(req).await?;
91
92        let user_role = user.role().unwrap_or("user");
93        if user_role != self.config.admin_role {
94            return Err(AuthError::forbidden(
95                "You do not have permission to access this resource",
96            ));
97        }
98
99        Ok((user, session))
100    }
101
102    async fn handle_set_role<DB: DatabaseAdapter>(
103        &self,
104        req: &AuthRequest,
105        ctx: &AuthContext<DB>,
106    ) -> AuthResult<AuthResponse> {
107        let (_admin_user, _admin_session) = self.require_admin(req, ctx).await?;
108        let body: SetRoleRequest = match better_auth_core::validate_request_body(req) {
109            Ok(v) => v,
110            Err(resp) => return Ok(resp),
111        };
112        let response = set_role_core(&body, ctx).await?;
113        AuthResponse::json(200, &response).map_err(AuthError::from)
114    }
115
116    async fn handle_create_user<DB: DatabaseAdapter>(
117        &self,
118        req: &AuthRequest,
119        ctx: &AuthContext<DB>,
120    ) -> AuthResult<AuthResponse> {
121        let (_admin_user, _admin_session) = self.require_admin(req, ctx).await?;
122        let body: CreateUserRequest = match better_auth_core::validate_request_body(req) {
123            Ok(v) => v,
124            Err(resp) => return Ok(resp),
125        };
126        let response = create_user_core(&body, &self.config, ctx).await?;
127        AuthResponse::json(200, &response).map_err(AuthError::from)
128    }
129
130    async fn handle_list_users<DB: DatabaseAdapter>(
131        &self,
132        req: &AuthRequest,
133        ctx: &AuthContext<DB>,
134    ) -> AuthResult<AuthResponse> {
135        let (_admin_user, _admin_session) = self.require_admin(req, ctx).await?;
136        let query = ListUsersQueryParams {
137            limit: req.query.get("limit").and_then(|v| v.parse().ok()),
138            offset: req.query.get("offset").and_then(|v| v.parse().ok()),
139            search_field: req.query.get("searchField").cloned(),
140            search_value: req.query.get("searchValue").cloned(),
141            search_operator: req.query.get("searchOperator").cloned(),
142            sort_by: req.query.get("sortBy").cloned(),
143            sort_direction: req.query.get("sortDirection").cloned(),
144            filter_field: req.query.get("filterField").cloned(),
145            filter_value: req.query.get("filterValue").cloned(),
146            filter_operator: req.query.get("filterOperator").cloned(),
147        };
148        let response = list_users_core(&query, &self.config, ctx).await?;
149        AuthResponse::json(200, &response).map_err(AuthError::from)
150    }
151
152    async fn handle_list_user_sessions<DB: DatabaseAdapter>(
153        &self,
154        req: &AuthRequest,
155        ctx: &AuthContext<DB>,
156    ) -> AuthResult<AuthResponse> {
157        let (_admin_user, _admin_session) = self.require_admin(req, ctx).await?;
158        let body: UserIdRequest = match better_auth_core::validate_request_body(req) {
159            Ok(v) => v,
160            Err(resp) => return Ok(resp),
161        };
162        let response = list_user_sessions_core(&body, ctx).await?;
163        AuthResponse::json(200, &response).map_err(AuthError::from)
164    }
165
166    async fn handle_ban_user<DB: DatabaseAdapter>(
167        &self,
168        req: &AuthRequest,
169        ctx: &AuthContext<DB>,
170    ) -> AuthResult<AuthResponse> {
171        let (admin_user, _admin_session) = self.require_admin(req, ctx).await?;
172        let body: BanUserRequest = match better_auth_core::validate_request_body(req) {
173            Ok(v) => v,
174            Err(resp) => return Ok(resp),
175        };
176        let response = ban_user_core(&body, admin_user.id(), &self.config, ctx).await?;
177        AuthResponse::json(200, &response).map_err(AuthError::from)
178    }
179
180    async fn handle_unban_user<DB: DatabaseAdapter>(
181        &self,
182        req: &AuthRequest,
183        ctx: &AuthContext<DB>,
184    ) -> AuthResult<AuthResponse> {
185        let (_admin_user, _admin_session) = self.require_admin(req, ctx).await?;
186        let body: UserIdRequest = match better_auth_core::validate_request_body(req) {
187            Ok(v) => v,
188            Err(resp) => return Ok(resp),
189        };
190        let response = unban_user_core(&body, ctx).await?;
191        AuthResponse::json(200, &response).map_err(AuthError::from)
192    }
193
194    async fn handle_impersonate_user<DB: DatabaseAdapter>(
195        &self,
196        req: &AuthRequest,
197        ctx: &AuthContext<DB>,
198    ) -> AuthResult<AuthResponse> {
199        let (admin_user, _admin_session) = self.require_admin(req, ctx).await?;
200        let body: UserIdRequest = match better_auth_core::validate_request_body(req) {
201            Ok(v) => v,
202            Err(resp) => return Ok(resp),
203        };
204        let (response, token) = impersonate_user_core(
205            &body,
206            admin_user.id(),
207            req.headers.get("x-forwarded-for").map(|s| s.as_str()),
208            req.headers.get("user-agent").map(|s| s.as_str()),
209            ctx,
210        )
211        .await?;
212        let cookie_header = create_session_cookie(&token, &ctx.config);
213        Ok(AuthResponse::json(200, &response)?.with_header("Set-Cookie", cookie_header))
214    }
215
216    async fn handle_stop_impersonating<DB: DatabaseAdapter>(
217        &self,
218        req: &AuthRequest,
219        ctx: &AuthContext<DB>,
220    ) -> AuthResult<AuthResponse> {
221        let session_manager = ctx.session_manager();
222        let token = session_manager
223            .extract_session_token(req)
224            .ok_or(AuthError::Unauthenticated)?;
225        let session = session_manager
226            .get_session(&token)
227            .await?
228            .ok_or(AuthError::Unauthenticated)?;
229        let (response, new_token) = stop_impersonating_core(
230            &session,
231            &token,
232            req.headers.get("x-forwarded-for").map(|s| s.as_str()),
233            req.headers.get("user-agent").map(|s| s.as_str()),
234            ctx,
235        )
236        .await?;
237        let cookie_header = create_session_cookie(&new_token, &ctx.config);
238        Ok(AuthResponse::json(200, &response)?.with_header("Set-Cookie", cookie_header))
239    }
240
241    async fn handle_revoke_user_session<DB: DatabaseAdapter>(
242        &self,
243        req: &AuthRequest,
244        ctx: &AuthContext<DB>,
245    ) -> AuthResult<AuthResponse> {
246        let (_admin_user, _admin_session) = self.require_admin(req, ctx).await?;
247        let body: RevokeSessionRequest = match better_auth_core::validate_request_body(req) {
248            Ok(v) => v,
249            Err(resp) => return Ok(resp),
250        };
251        let response = revoke_user_session_core(&body, ctx).await?;
252        AuthResponse::json(200, &response).map_err(AuthError::from)
253    }
254
255    async fn handle_revoke_user_sessions<DB: DatabaseAdapter>(
256        &self,
257        req: &AuthRequest,
258        ctx: &AuthContext<DB>,
259    ) -> AuthResult<AuthResponse> {
260        let (_admin_user, _admin_session) = self.require_admin(req, ctx).await?;
261        let body: UserIdRequest = match better_auth_core::validate_request_body(req) {
262            Ok(v) => v,
263            Err(resp) => return Ok(resp),
264        };
265        let response = revoke_user_sessions_core(&body, ctx).await?;
266        AuthResponse::json(200, &response).map_err(AuthError::from)
267    }
268
269    async fn handle_remove_user<DB: DatabaseAdapter>(
270        &self,
271        req: &AuthRequest,
272        ctx: &AuthContext<DB>,
273    ) -> AuthResult<AuthResponse> {
274        let (admin_user, _admin_session) = self.require_admin(req, ctx).await?;
275        let body: UserIdRequest = match better_auth_core::validate_request_body(req) {
276            Ok(v) => v,
277            Err(resp) => return Ok(resp),
278        };
279        let response = remove_user_core(&body, admin_user.id(), ctx).await?;
280        AuthResponse::json(200, &response).map_err(AuthError::from)
281    }
282
283    async fn handle_set_user_password<DB: DatabaseAdapter>(
284        &self,
285        req: &AuthRequest,
286        ctx: &AuthContext<DB>,
287    ) -> AuthResult<AuthResponse> {
288        let (_admin_user, _admin_session) = self.require_admin(req, ctx).await?;
289        let body: SetUserPasswordRequest = match better_auth_core::validate_request_body(req) {
290            Ok(v) => v,
291            Err(resp) => return Ok(resp),
292        };
293        let response = set_user_password_core(&body, ctx).await?;
294        AuthResponse::json(200, &response).map_err(AuthError::from)
295    }
296
297    async fn handle_has_permission<DB: DatabaseAdapter>(
298        &self,
299        req: &AuthRequest,
300        ctx: &AuthContext<DB>,
301    ) -> AuthResult<AuthResponse> {
302        let (user, _session) = ctx.require_session(req).await?;
303        let body: HasPermissionRequest = match better_auth_core::validate_request_body(req) {
304            Ok(v) => v,
305            Err(resp) => return Ok(resp),
306        };
307        let response = has_permission_core::<DB>(&body, &user, &self.config).await?;
308        AuthResponse::json(200, &response).map_err(AuthError::from)
309    }
310}
311
312// ---------------------------------------------------------------------------
313// Axum integration
314// ---------------------------------------------------------------------------
315
316#[cfg(feature = "axum")]
317mod axum_impl {
318    use super::*;
319    use std::sync::Arc;
320
321    use axum::Json;
322    use axum::extract::{Extension, Query, State};
323    use axum::http::header;
324    use better_auth_core::entity::AuthSession;
325    use better_auth_core::{AdminRole, AdminSession, AuthState, CurrentSession, ValidatedJson};
326
327    #[derive(Clone)]
328    struct PluginState {
329        config: AdminConfig,
330    }
331
332    async fn handle_set_role<DB: DatabaseAdapter>(
333        State(state): State<AuthState<DB>>,
334        Extension(ps): Extension<Arc<PluginState>>,
335        AdminSession { .. }: AdminSession<DB>,
336        ValidatedJson(body): ValidatedJson<SetRoleRequest>,
337    ) -> Result<Json<UserResponse<DB::User>>, AuthError> {
338        let ctx = state.to_context();
339        let response = set_role_core(&body, &ctx).await?;
340        Ok(Json(response))
341    }
342
343    async fn handle_create_user<DB: DatabaseAdapter>(
344        State(state): State<AuthState<DB>>,
345        Extension(ps): Extension<Arc<PluginState>>,
346        AdminSession { .. }: AdminSession<DB>,
347        ValidatedJson(body): ValidatedJson<CreateUserRequest>,
348    ) -> Result<Json<UserResponse<DB::User>>, AuthError> {
349        let ctx = state.to_context();
350        let response = create_user_core(&body, &ps.config, &ctx).await?;
351        Ok(Json(response))
352    }
353
354    async fn handle_list_users<DB: DatabaseAdapter>(
355        State(state): State<AuthState<DB>>,
356        Extension(ps): Extension<Arc<PluginState>>,
357        AdminSession { .. }: AdminSession<DB>,
358        Query(query): Query<ListUsersQueryParams>,
359    ) -> Result<Json<ListUsersResponse<DB::User>>, AuthError> {
360        let ctx = state.to_context();
361        let response = list_users_core(&query, &ps.config, &ctx).await?;
362        Ok(Json(response))
363    }
364
365    async fn handle_list_user_sessions<DB: DatabaseAdapter>(
366        State(state): State<AuthState<DB>>,
367        AdminSession { .. }: AdminSession<DB>,
368        ValidatedJson(body): ValidatedJson<UserIdRequest>,
369    ) -> Result<Json<ListSessionsResponse<DB::Session>>, AuthError> {
370        let ctx = state.to_context();
371        let response = list_user_sessions_core(&body, &ctx).await?;
372        Ok(Json(response))
373    }
374
375    async fn handle_ban_user<DB: DatabaseAdapter>(
376        State(state): State<AuthState<DB>>,
377        Extension(ps): Extension<Arc<PluginState>>,
378        AdminSession { user, .. }: AdminSession<DB>,
379        ValidatedJson(body): ValidatedJson<BanUserRequest>,
380    ) -> Result<Json<UserResponse<DB::User>>, AuthError> {
381        let ctx = state.to_context();
382        let response = ban_user_core(&body, user.id(), &ps.config, &ctx).await?;
383        Ok(Json(response))
384    }
385
386    async fn handle_unban_user<DB: DatabaseAdapter>(
387        State(state): State<AuthState<DB>>,
388        AdminSession { .. }: AdminSession<DB>,
389        ValidatedJson(body): ValidatedJson<UserIdRequest>,
390    ) -> Result<Json<UserResponse<DB::User>>, AuthError> {
391        let ctx = state.to_context();
392        let response = unban_user_core(&body, &ctx).await?;
393        Ok(Json(response))
394    }
395
396    async fn handle_impersonate_user<DB: DatabaseAdapter>(
397        State(state): State<AuthState<DB>>,
398        AdminSession { user, .. }: AdminSession<DB>,
399        ValidatedJson(body): ValidatedJson<UserIdRequest>,
400    ) -> Result<
401        (
402            [(header::HeaderName, String); 1],
403            Json<SessionUserResponse<DB::Session, DB::User>>,
404        ),
405        AuthError,
406    > {
407        let ctx = state.to_context();
408        let (response, token) = impersonate_user_core(&body, user.id(), None, None, &ctx).await?;
409        let cookie = state.session_cookie(&token);
410        Ok(([(header::SET_COOKIE, cookie)], Json(response)))
411    }
412
413    async fn handle_stop_impersonating<DB: DatabaseAdapter>(
414        State(state): State<AuthState<DB>>,
415        CurrentSession { session, .. }: CurrentSession<DB>,
416    ) -> Result<
417        (
418            [(header::HeaderName, String); 1],
419            Json<SessionUserResponse<DB::Session, DB::User>>,
420        ),
421        AuthError,
422    > {
423        let ctx = state.to_context();
424        let token = session.token().to_string();
425        let (response, new_token) =
426            stop_impersonating_core(&session, &token, None, None, &ctx).await?;
427        let cookie = state.session_cookie(&new_token);
428        Ok(([(header::SET_COOKIE, cookie)], Json(response)))
429    }
430
431    async fn handle_revoke_user_session<DB: DatabaseAdapter>(
432        State(state): State<AuthState<DB>>,
433        AdminSession { .. }: AdminSession<DB>,
434        ValidatedJson(body): ValidatedJson<RevokeSessionRequest>,
435    ) -> Result<Json<SuccessResponse>, AuthError> {
436        let ctx = state.to_context();
437        let response = revoke_user_session_core(&body, &ctx).await?;
438        Ok(Json(response))
439    }
440
441    async fn handle_revoke_user_sessions<DB: DatabaseAdapter>(
442        State(state): State<AuthState<DB>>,
443        AdminSession { .. }: AdminSession<DB>,
444        ValidatedJson(body): ValidatedJson<UserIdRequest>,
445    ) -> Result<Json<SuccessResponse>, AuthError> {
446        let ctx = state.to_context();
447        let response = revoke_user_sessions_core(&body, &ctx).await?;
448        Ok(Json(response))
449    }
450
451    async fn handle_remove_user<DB: DatabaseAdapter>(
452        State(state): State<AuthState<DB>>,
453        AdminSession { user, .. }: AdminSession<DB>,
454        ValidatedJson(body): ValidatedJson<UserIdRequest>,
455    ) -> Result<Json<SuccessResponse>, AuthError> {
456        let ctx = state.to_context();
457        let response = remove_user_core(&body, user.id(), &ctx).await?;
458        Ok(Json(response))
459    }
460
461    async fn handle_set_user_password<DB: DatabaseAdapter>(
462        State(state): State<AuthState<DB>>,
463        AdminSession { .. }: AdminSession<DB>,
464        ValidatedJson(body): ValidatedJson<SetUserPasswordRequest>,
465    ) -> Result<Json<StatusResponse>, AuthError> {
466        let ctx = state.to_context();
467        let response = set_user_password_core(&body, &ctx).await?;
468        Ok(Json(response))
469    }
470
471    async fn handle_has_permission<DB: DatabaseAdapter>(
472        State(state): State<AuthState<DB>>,
473        Extension(ps): Extension<Arc<PluginState>>,
474        CurrentSession { user, .. }: CurrentSession<DB>,
475        ValidatedJson(body): ValidatedJson<HasPermissionRequest>,
476    ) -> Result<Json<PermissionResponse>, AuthError> {
477        let response = has_permission_core::<DB>(&body, &user, &ps.config).await?;
478        Ok(Json(response))
479    }
480
481    impl<DB: DatabaseAdapter> better_auth_core::AxumPlugin<DB> for AdminPlugin {
482        fn name(&self) -> &'static str {
483            "admin"
484        }
485
486        fn router(&self) -> axum::Router<AuthState<DB>> {
487            use axum::routing::{get, post};
488
489            let plugin_state = Arc::new(PluginState {
490                config: self.config.clone(),
491            });
492            axum::Router::new()
493                .route("/admin/set-role", post(handle_set_role::<DB>))
494                .route("/admin/create-user", post(handle_create_user::<DB>))
495                .route("/admin/list-users", get(handle_list_users::<DB>))
496                .route(
497                    "/admin/list-user-sessions",
498                    post(handle_list_user_sessions::<DB>),
499                )
500                .route("/admin/ban-user", post(handle_ban_user::<DB>))
501                .route("/admin/unban-user", post(handle_unban_user::<DB>))
502                .route(
503                    "/admin/impersonate-user",
504                    post(handle_impersonate_user::<DB>),
505                )
506                .route(
507                    "/admin/stop-impersonating",
508                    post(handle_stop_impersonating::<DB>),
509                )
510                .route(
511                    "/admin/revoke-user-session",
512                    post(handle_revoke_user_session::<DB>),
513                )
514                .route(
515                    "/admin/revoke-user-sessions",
516                    post(handle_revoke_user_sessions::<DB>),
517                )
518                .route("/admin/remove-user", post(handle_remove_user::<DB>))
519                .route(
520                    "/admin/set-user-password",
521                    post(handle_set_user_password::<DB>),
522                )
523                .route("/admin/has-permission", post(handle_has_permission::<DB>))
524                .layer(Extension(plugin_state))
525                .layer(Extension(AdminRole(self.config.admin_role.clone())))
526        }
527    }
528}