Skip to main content

better_auth_api/plugins/
admin.rs

1use async_trait::async_trait;
2use chrono::{Duration, Utc};
3use serde::{Deserialize, Serialize};
4use validator::Validate;
5
6use better_auth_core::adapters::DatabaseAdapter;
7use better_auth_core::entity::{AuthAccount, AuthSession, AuthUser};
8use better_auth_core::{AuthContext, AuthPlugin, AuthRoute, ListUsersParams, SessionManager};
9use better_auth_core::{AuthError, AuthResult};
10use better_auth_core::{
11    AuthRequest, AuthResponse, CreateAccount, CreateSession, CreateUser, HttpMethod, UpdateUser,
12};
13
14use better_auth_core::utils::cookie_utils::create_session_cookie;
15
16use super::StatusResponse;
17
18// ---------------------------------------------------------------------------
19// Plugin & config
20// ---------------------------------------------------------------------------
21
22/// Admin plugin for user management operations.
23///
24/// Provides endpoints for creating users, listing users, banning/unbanning,
25/// role management, session management, password management, user impersonation,
26/// and permission checks.
27///
28/// All endpoints require an authenticated session with the `admin` role.
29pub struct AdminPlugin {
30    config: AdminConfig,
31}
32
33/// Configuration for the admin plugin.
34#[derive(Debug, Clone)]
35pub struct AdminConfig {
36    /// The role required to access admin endpoints (default: `"admin"`).
37    pub admin_role: String,
38    /// Default role assigned to newly created users (default: `"user"`).
39    pub default_user_role: String,
40    /// Whether to allow banning other admins (default: `false`).
41    pub allow_ban_admin: bool,
42    /// Default number of users returned in list-users (default: 100).
43    pub default_page_limit: usize,
44    /// Maximum number of users returned in list-users (default: 500).
45    pub max_page_limit: usize,
46}
47
48impl Default for AdminConfig {
49    fn default() -> Self {
50        Self {
51            admin_role: "admin".to_string(),
52            default_user_role: "user".to_string(),
53            allow_ban_admin: false,
54            default_page_limit: 100,
55            max_page_limit: 500,
56        }
57    }
58}
59
60impl AdminPlugin {
61    pub fn new() -> Self {
62        Self {
63            config: AdminConfig::default(),
64        }
65    }
66
67    pub fn with_config(config: AdminConfig) -> Self {
68        Self { config }
69    }
70
71    pub fn admin_role(mut self, role: impl Into<String>) -> Self {
72        self.config.admin_role = role.into();
73        self
74    }
75
76    pub fn default_user_role(mut self, role: impl Into<String>) -> Self {
77        self.config.default_user_role = role.into();
78        self
79    }
80
81    pub fn allow_ban_admin(mut self, allow: bool) -> Self {
82        self.config.allow_ban_admin = allow;
83        self
84    }
85
86    pub fn default_page_limit(mut self, limit: usize) -> Self {
87        self.config.default_page_limit = limit;
88        self
89    }
90
91    pub fn max_page_limit(mut self, limit: usize) -> Self {
92        self.config.max_page_limit = limit;
93        self
94    }
95}
96
97impl Default for AdminPlugin {
98    fn default() -> Self {
99        Self::new()
100    }
101}
102
103// ---------------------------------------------------------------------------
104// Request types
105// ---------------------------------------------------------------------------
106
107#[derive(Debug, Deserialize, Validate)]
108struct SetRoleRequest {
109    #[serde(rename = "userId")]
110    #[validate(length(min = 1, message = "userId is required"))]
111    user_id: String,
112    #[validate(length(min = 1, message = "role is required"))]
113    role: String,
114}
115
116#[derive(Debug, Deserialize, Validate)]
117struct CreateUserRequest {
118    #[validate(email(message = "Invalid email address"))]
119    email: String,
120    #[validate(length(min = 1, message = "Password is required"))]
121    password: String,
122    #[validate(length(min = 1, message = "Name is required"))]
123    name: String,
124    role: Option<String>,
125    data: Option<serde_json::Value>,
126}
127
128#[derive(Debug, Deserialize, Validate)]
129struct UserIdRequest {
130    #[serde(rename = "userId")]
131    #[validate(length(min = 1, message = "userId is required"))]
132    user_id: String,
133}
134
135#[derive(Debug, Deserialize, Validate)]
136struct BanUserRequest {
137    #[serde(rename = "userId")]
138    #[validate(length(min = 1, message = "userId is required"))]
139    user_id: String,
140    #[serde(rename = "banReason")]
141    ban_reason: Option<String>,
142    /// Number of seconds until the ban expires.
143    #[serde(rename = "banExpiresIn")]
144    ban_expires_in: Option<i64>,
145}
146
147#[derive(Debug, Deserialize, Validate)]
148struct RevokeSessionRequest {
149    #[serde(rename = "sessionToken")]
150    #[validate(length(min = 1, message = "sessionToken is required"))]
151    session_token: String,
152}
153
154#[derive(Debug, Deserialize, Validate)]
155struct SetUserPasswordRequest {
156    #[serde(rename = "userId")]
157    #[validate(length(min = 1, message = "userId is required"))]
158    user_id: String,
159    #[serde(rename = "newPassword")]
160    #[validate(length(min = 1, message = "newPassword is required"))]
161    new_password: String,
162}
163
164#[derive(Debug, Deserialize, Validate)]
165struct HasPermissionRequest {
166    permission: Option<serde_json::Value>,
167    permissions: Option<serde_json::Value>,
168}
169
170// ---------------------------------------------------------------------------
171// Response types
172// ---------------------------------------------------------------------------
173
174#[derive(Debug, Serialize)]
175struct UserResponse<U: Serialize> {
176    user: U,
177}
178
179#[derive(Debug, Serialize)]
180struct SessionUserResponse<S: Serialize, U: Serialize> {
181    session: S,
182    user: U,
183}
184
185#[derive(Debug, Serialize)]
186struct ListUsersResponse<U: Serialize> {
187    users: Vec<U>,
188    total: usize,
189    limit: usize,
190    offset: usize,
191}
192
193#[derive(Debug, Serialize)]
194struct ListSessionsResponse<S: Serialize> {
195    sessions: Vec<S>,
196}
197
198#[derive(Debug, Serialize)]
199struct SuccessResponse {
200    success: bool,
201}
202
203#[derive(Debug, Serialize)]
204struct PermissionResponse {
205    success: bool,
206    #[serde(skip_serializing_if = "Option::is_none")]
207    error: Option<String>,
208}
209
210// ---------------------------------------------------------------------------
211// Plugin trait implementation
212// ---------------------------------------------------------------------------
213
214#[async_trait]
215impl<DB: DatabaseAdapter> AuthPlugin<DB> for AdminPlugin {
216    fn name(&self) -> &'static str {
217        "admin"
218    }
219
220    fn routes(&self) -> Vec<AuthRoute> {
221        vec![
222            AuthRoute::post("/admin/set-role", "admin_set_role"),
223            AuthRoute::post("/admin/create-user", "admin_create_user"),
224            AuthRoute::get("/admin/list-users", "admin_list_users"),
225            AuthRoute::post("/admin/list-user-sessions", "admin_list_user_sessions"),
226            AuthRoute::post("/admin/ban-user", "admin_ban_user"),
227            AuthRoute::post("/admin/unban-user", "admin_unban_user"),
228            AuthRoute::post("/admin/impersonate-user", "admin_impersonate_user"),
229            AuthRoute::post("/admin/stop-impersonating", "admin_stop_impersonating"),
230            AuthRoute::post("/admin/revoke-user-session", "admin_revoke_user_session"),
231            AuthRoute::post("/admin/revoke-user-sessions", "admin_revoke_user_sessions"),
232            AuthRoute::post("/admin/remove-user", "admin_remove_user"),
233            AuthRoute::post("/admin/set-user-password", "admin_set_user_password"),
234            AuthRoute::post("/admin/has-permission", "admin_has_permission"),
235        ]
236    }
237
238    async fn on_request(
239        &self,
240        req: &AuthRequest,
241        ctx: &AuthContext<DB>,
242    ) -> AuthResult<Option<AuthResponse>> {
243        match (req.method(), req.path()) {
244            (HttpMethod::Post, "/admin/set-role") => {
245                Ok(Some(self.handle_set_role(req, ctx).await?))
246            }
247            (HttpMethod::Post, "/admin/create-user") => {
248                Ok(Some(self.handle_create_user(req, ctx).await?))
249            }
250            (HttpMethod::Get, "/admin/list-users") => {
251                Ok(Some(self.handle_list_users(req, ctx).await?))
252            }
253            (HttpMethod::Post, "/admin/list-user-sessions") => {
254                Ok(Some(self.handle_list_user_sessions(req, ctx).await?))
255            }
256            (HttpMethod::Post, "/admin/ban-user") => {
257                Ok(Some(self.handle_ban_user(req, ctx).await?))
258            }
259            (HttpMethod::Post, "/admin/unban-user") => {
260                Ok(Some(self.handle_unban_user(req, ctx).await?))
261            }
262            (HttpMethod::Post, "/admin/impersonate-user") => {
263                Ok(Some(self.handle_impersonate_user(req, ctx).await?))
264            }
265            (HttpMethod::Post, "/admin/stop-impersonating") => {
266                Ok(Some(self.handle_stop_impersonating(req, ctx).await?))
267            }
268            (HttpMethod::Post, "/admin/revoke-user-session") => {
269                Ok(Some(self.handle_revoke_user_session(req, ctx).await?))
270            }
271            (HttpMethod::Post, "/admin/revoke-user-sessions") => {
272                Ok(Some(self.handle_revoke_user_sessions(req, ctx).await?))
273            }
274            (HttpMethod::Post, "/admin/remove-user") => {
275                Ok(Some(self.handle_remove_user(req, ctx).await?))
276            }
277            (HttpMethod::Post, "/admin/set-user-password") => {
278                Ok(Some(self.handle_set_user_password(req, ctx).await?))
279            }
280            (HttpMethod::Post, "/admin/has-permission") => {
281                Ok(Some(self.handle_has_permission(req, ctx).await?))
282            }
283            _ => Ok(None),
284        }
285    }
286}
287
288// ---------------------------------------------------------------------------
289// Handler implementations
290// ---------------------------------------------------------------------------
291
292impl AdminPlugin {
293    // -- Auth helpers --------------------------------------------------------
294
295    /// Authenticate the caller and verify they have the admin role.
296    async fn require_admin<DB: DatabaseAdapter>(
297        &self,
298        req: &AuthRequest,
299        ctx: &AuthContext<DB>,
300    ) -> AuthResult<(DB::User, DB::Session)> {
301        let (user, session) = ctx.require_session(req).await?;
302
303        // Check admin role
304        let user_role = user.role().unwrap_or("user");
305        if user_role != self.config.admin_role {
306            return Err(AuthError::forbidden(
307                "You do not have permission to access this resource",
308            ));
309        }
310
311        Ok((user, session))
312    }
313
314    // -- Handlers -----------------------------------------------------------
315
316    /// POST /admin/set-role — Set the role of a user.
317    async fn handle_set_role<DB: DatabaseAdapter>(
318        &self,
319        req: &AuthRequest,
320        ctx: &AuthContext<DB>,
321    ) -> AuthResult<AuthResponse> {
322        let (_admin_user, _admin_session) = self.require_admin(req, ctx).await?;
323
324        let body: SetRoleRequest = match better_auth_core::validate_request_body(req) {
325            Ok(v) => v,
326            Err(resp) => return Ok(resp),
327        };
328
329        // Find target user
330        let _target = ctx
331            .database
332            .get_user_by_id(&body.user_id)
333            .await?
334            .ok_or_else(|| AuthError::not_found("User not found"))?;
335
336        let update = UpdateUser {
337            role: Some(body.role),
338            ..Default::default()
339        };
340
341        let updated_user = ctx.database.update_user(&body.user_id, update).await?;
342
343        let response = UserResponse { user: updated_user };
344        AuthResponse::json(200, &response).map_err(AuthError::from)
345    }
346
347    /// POST /admin/create-user — Create a new user (admin only).
348    async fn handle_create_user<DB: DatabaseAdapter>(
349        &self,
350        req: &AuthRequest,
351        ctx: &AuthContext<DB>,
352    ) -> AuthResult<AuthResponse> {
353        let (_admin_user, _admin_session) = self.require_admin(req, ctx).await?;
354
355        let body: CreateUserRequest = match better_auth_core::validate_request_body(req) {
356            Ok(v) => v,
357            Err(resp) => return Ok(resp),
358        };
359
360        // Check if user with email already exists
361        if ctx.database.get_user_by_email(&body.email).await?.is_some() {
362            return Err(AuthError::conflict("A user with this email already exists"));
363        }
364
365        // Validate password length
366        if body.password.len() < ctx.config.password.min_length {
367            return Err(AuthError::bad_request(format!(
368                "Password must be at least {} characters long",
369                ctx.config.password.min_length
370            )));
371        }
372
373        // Hash the password
374        let password_hash = better_auth_core::hash_password(None, &body.password).await?;
375
376        let role = body
377            .role
378            .unwrap_or_else(|| self.config.default_user_role.clone());
379
380        // Normalize metadata to always be a JSON object and include the password_hash
381        let metadata_value = body.data.unwrap_or(serde_json::json!({}));
382        let metadata = if let serde_json::Value::Object(mut obj) = metadata_value {
383            obj.insert(
384                "password_hash".to_string(),
385                serde_json::json!(password_hash),
386            );
387            serde_json::Value::Object(obj)
388        } else {
389            let mut obj = serde_json::Map::new();
390            obj.insert(
391                "password_hash".to_string(),
392                serde_json::json!(password_hash),
393            );
394            serde_json::Value::Object(obj)
395        };
396
397        let create_user = CreateUser::new()
398            .with_email(&body.email)
399            .with_name(&body.name)
400            .with_role(role)
401            .with_email_verified(true)
402            .with_metadata(metadata);
403
404        let user = ctx.database.create_user(create_user).await?;
405
406        // Create a credential account for the user
407        ctx.database
408            .create_account(CreateAccount {
409                user_id: user.id().to_string(),
410                account_id: user.id().to_string(),
411                provider_id: "credential".to_string(),
412                access_token: None,
413                refresh_token: None,
414                id_token: None,
415                access_token_expires_at: None,
416                refresh_token_expires_at: None,
417                scope: None,
418                password: Some(password_hash),
419            })
420            .await?;
421
422        let response = UserResponse { user };
423        AuthResponse::json(200, &response).map_err(AuthError::from)
424    }
425
426    /// GET /admin/list-users — List users with optional search, filter, sort, and pagination.
427    async fn handle_list_users<DB: DatabaseAdapter>(
428        &self,
429        req: &AuthRequest,
430        ctx: &AuthContext<DB>,
431    ) -> AuthResult<AuthResponse> {
432        let (_admin_user, _admin_session) = self.require_admin(req, ctx).await?;
433
434        let limit = req
435            .query
436            .get("limit")
437            .and_then(|v| v.parse::<usize>().ok())
438            .unwrap_or(self.config.default_page_limit)
439            .min(self.config.max_page_limit);
440
441        let offset = req
442            .query
443            .get("offset")
444            .and_then(|v| v.parse::<usize>().ok())
445            .unwrap_or(0);
446
447        let params = ListUsersParams {
448            limit: Some(limit),
449            offset: Some(offset),
450            search_field: req.query.get("searchField").cloned(),
451            search_value: req.query.get("searchValue").cloned(),
452            search_operator: req.query.get("searchOperator").cloned(),
453            sort_by: req.query.get("sortBy").cloned(),
454            sort_direction: req.query.get("sortDirection").cloned(),
455            filter_field: req.query.get("filterField").cloned(),
456            filter_value: req.query.get("filterValue").cloned(),
457            filter_operator: req.query.get("filterOperator").cloned(),
458        };
459
460        let (users, total) = ctx.database.list_users(params).await?;
461
462        let response = ListUsersResponse {
463            users,
464            total,
465            limit,
466            offset,
467        };
468        AuthResponse::json(200, &response).map_err(AuthError::from)
469    }
470
471    /// POST /admin/list-user-sessions — List all active sessions for a user.
472    async fn handle_list_user_sessions<DB: DatabaseAdapter>(
473        &self,
474        req: &AuthRequest,
475        ctx: &AuthContext<DB>,
476    ) -> AuthResult<AuthResponse> {
477        let (_admin_user, _admin_session) = self.require_admin(req, ctx).await?;
478
479        let body: UserIdRequest = match better_auth_core::validate_request_body(req) {
480            Ok(v) => v,
481            Err(resp) => return Ok(resp),
482        };
483
484        // Verify the target user exists
485        let _target = ctx
486            .database
487            .get_user_by_id(&body.user_id)
488            .await?
489            .ok_or_else(|| AuthError::not_found("User not found"))?;
490
491        let session_manager = SessionManager::new(ctx.config.clone(), ctx.database.clone());
492        let sessions = session_manager.list_user_sessions(&body.user_id).await?;
493
494        let response = ListSessionsResponse { sessions };
495        AuthResponse::json(200, &response).map_err(AuthError::from)
496    }
497
498    /// POST /admin/ban-user — Ban a user.
499    async fn handle_ban_user<DB: DatabaseAdapter>(
500        &self,
501        req: &AuthRequest,
502        ctx: &AuthContext<DB>,
503    ) -> AuthResult<AuthResponse> {
504        let (admin_user, _admin_session) = self.require_admin(req, ctx).await?;
505
506        let body: BanUserRequest = match better_auth_core::validate_request_body(req) {
507            Ok(v) => v,
508            Err(resp) => return Ok(resp),
509        };
510
511        // Prevent banning yourself
512        if body.user_id == admin_user.id() {
513            return Err(AuthError::bad_request("You cannot ban yourself"));
514        }
515
516        let target = ctx
517            .database
518            .get_user_by_id(&body.user_id)
519            .await?
520            .ok_or_else(|| AuthError::not_found("User not found"))?;
521
522        // Prevent banning other admins unless explicitly allowed
523        if !self.config.allow_ban_admin && target.role().unwrap_or("user") == self.config.admin_role
524        {
525            return Err(AuthError::forbidden("Cannot ban an admin user"));
526        }
527
528        let ban_expires = body
529            .ban_expires_in
530            .and_then(Duration::try_seconds)
531            .map(|d| Utc::now() + d);
532
533        let update = UpdateUser {
534            banned: Some(true),
535            ban_reason: body.ban_reason,
536            ban_expires,
537            ..Default::default()
538        };
539
540        let updated_user = ctx.database.update_user(&body.user_id, update).await?;
541
542        // Revoke all sessions for the banned user
543        let session_manager = SessionManager::new(ctx.config.clone(), ctx.database.clone());
544        session_manager
545            .revoke_all_user_sessions(&body.user_id)
546            .await?;
547
548        let response = UserResponse { user: updated_user };
549        AuthResponse::json(200, &response).map_err(AuthError::from)
550    }
551
552    /// POST /admin/unban-user — Unban a user.
553    async fn handle_unban_user<DB: DatabaseAdapter>(
554        &self,
555        req: &AuthRequest,
556        ctx: &AuthContext<DB>,
557    ) -> AuthResult<AuthResponse> {
558        let (_admin_user, _admin_session) = self.require_admin(req, ctx).await?;
559
560        let body: UserIdRequest = match better_auth_core::validate_request_body(req) {
561            Ok(v) => v,
562            Err(resp) => return Ok(resp),
563        };
564
565        let _target = ctx
566            .database
567            .get_user_by_id(&body.user_id)
568            .await?
569            .ok_or_else(|| AuthError::not_found("User not found"))?;
570
571        let update = UpdateUser {
572            banned: Some(false),
573            ban_reason: None,
574            ban_expires: None,
575            ..Default::default()
576        };
577
578        // The adapter's apply_update clears ban_reason and ban_expires
579        // when banned is explicitly set to false.
580        let updated_user = ctx.database.update_user(&body.user_id, update).await?;
581
582        let response = UserResponse { user: updated_user };
583        AuthResponse::json(200, &response).map_err(AuthError::from)
584    }
585
586    /// POST /admin/impersonate-user — Create an impersonation session.
587    async fn handle_impersonate_user<DB: DatabaseAdapter>(
588        &self,
589        req: &AuthRequest,
590        ctx: &AuthContext<DB>,
591    ) -> AuthResult<AuthResponse> {
592        let (admin_user, _admin_session) = self.require_admin(req, ctx).await?;
593
594        let body: UserIdRequest = match better_auth_core::validate_request_body(req) {
595            Ok(v) => v,
596            Err(resp) => return Ok(resp),
597        };
598
599        // Cannot impersonate yourself
600        if body.user_id == admin_user.id() {
601            return Err(AuthError::bad_request("Cannot impersonate yourself"));
602        }
603
604        let target = ctx
605            .database
606            .get_user_by_id(&body.user_id)
607            .await?
608            .ok_or_else(|| AuthError::not_found("User not found"))?;
609
610        // Create an impersonation session
611        let expires_at = Utc::now() + ctx.config.session.expires_in;
612        let create_session = CreateSession {
613            user_id: target.id().to_string(),
614            expires_at,
615            ip_address: req.headers.get("x-forwarded-for").cloned(),
616            user_agent: req.headers.get("user-agent").cloned(),
617            impersonated_by: Some(admin_user.id().to_string()),
618            active_organization_id: None,
619        };
620
621        let session = ctx.database.create_session(create_session).await?;
622
623        let cookie_header = create_session_cookie(session.token(), ctx);
624        let response = SessionUserResponse {
625            session,
626            user: target,
627        };
628
629        Ok(AuthResponse::json(200, &response)?.with_header("Set-Cookie", cookie_header))
630    }
631
632    /// POST /admin/stop-impersonating — Stop the current impersonation session.
633    async fn handle_stop_impersonating<DB: DatabaseAdapter>(
634        &self,
635        req: &AuthRequest,
636        ctx: &AuthContext<DB>,
637    ) -> AuthResult<AuthResponse> {
638        let session_manager = SessionManager::new(ctx.config.clone(), ctx.database.clone());
639
640        let token = session_manager
641            .extract_session_token(req)
642            .ok_or(AuthError::Unauthenticated)?;
643
644        let session = session_manager
645            .get_session(&token)
646            .await?
647            .ok_or(AuthError::Unauthenticated)?;
648
649        // Must be an impersonation session
650        let admin_id = session
651            .impersonated_by()
652            .ok_or_else(|| {
653                AuthError::bad_request("Current session is not an impersonation session")
654            })?
655            .to_string();
656
657        // Delete the impersonation session
658        session_manager.delete_session(&token).await?;
659
660        // Look up the original admin user
661        let admin_user = ctx
662            .database
663            .get_user_by_id(&admin_id)
664            .await?
665            .ok_or(AuthError::UserNotFound)?;
666
667        // Create a new session for the admin user so the client
668        // transitions back to a valid admin session.
669        let expires_at = Utc::now() + ctx.config.session.expires_in;
670        let create_session = CreateSession {
671            user_id: admin_id,
672            expires_at,
673            ip_address: req.headers.get("x-forwarded-for").cloned(),
674            user_agent: req.headers.get("user-agent").cloned(),
675            impersonated_by: None,
676            active_organization_id: None,
677        };
678
679        let admin_session = ctx.database.create_session(create_session).await?;
680
681        let cookie_header = create_session_cookie(admin_session.token(), ctx);
682        let response = SessionUserResponse {
683            session: admin_session,
684            user: admin_user,
685        };
686
687        Ok(AuthResponse::json(200, &response)?.with_header("Set-Cookie", cookie_header))
688    }
689
690    /// POST /admin/revoke-user-session — Revoke a specific session by token.
691    async fn handle_revoke_user_session<DB: DatabaseAdapter>(
692        &self,
693        req: &AuthRequest,
694        ctx: &AuthContext<DB>,
695    ) -> AuthResult<AuthResponse> {
696        let (_admin_user, _admin_session) = self.require_admin(req, ctx).await?;
697
698        let body: RevokeSessionRequest = match better_auth_core::validate_request_body(req) {
699            Ok(v) => v,
700            Err(resp) => return Ok(resp),
701        };
702
703        let session_manager = SessionManager::new(ctx.config.clone(), ctx.database.clone());
704        session_manager.delete_session(&body.session_token).await?;
705
706        let response = SuccessResponse { success: true };
707        AuthResponse::json(200, &response).map_err(AuthError::from)
708    }
709
710    /// POST /admin/revoke-user-sessions — Revoke all sessions for a user.
711    async fn handle_revoke_user_sessions<DB: DatabaseAdapter>(
712        &self,
713        req: &AuthRequest,
714        ctx: &AuthContext<DB>,
715    ) -> AuthResult<AuthResponse> {
716        let (_admin_user, _admin_session) = self.require_admin(req, ctx).await?;
717
718        let body: UserIdRequest = match better_auth_core::validate_request_body(req) {
719            Ok(v) => v,
720            Err(resp) => return Ok(resp),
721        };
722
723        // Verify the target user exists
724        let _target = ctx
725            .database
726            .get_user_by_id(&body.user_id)
727            .await?
728            .ok_or_else(|| AuthError::not_found("User not found"))?;
729
730        let session_manager = SessionManager::new(ctx.config.clone(), ctx.database.clone());
731        session_manager
732            .revoke_all_user_sessions(&body.user_id)
733            .await?;
734
735        let response = SuccessResponse { success: true };
736        AuthResponse::json(200, &response).map_err(AuthError::from)
737    }
738
739    /// POST /admin/remove-user — Delete a user and all their data.
740    ///
741    /// **Note:** This endpoint deletes sessions, accounts, and the user record.
742    /// Data owned by other plugins (passkeys, two-factor settings, API keys,
743    /// organization memberships) is **not** cleaned up here.  Those records
744    /// should be removed through the respective plugin APIs or via database
745    /// cascade rules.
746    async fn handle_remove_user<DB: DatabaseAdapter>(
747        &self,
748        req: &AuthRequest,
749        ctx: &AuthContext<DB>,
750    ) -> AuthResult<AuthResponse> {
751        let (admin_user, _admin_session) = self.require_admin(req, ctx).await?;
752
753        let body: UserIdRequest = match better_auth_core::validate_request_body(req) {
754            Ok(v) => v,
755            Err(resp) => return Ok(resp),
756        };
757
758        // Prevent self-deletion
759        if body.user_id == admin_user.id() {
760            return Err(AuthError::bad_request("You cannot remove yourself"));
761        }
762
763        // Verify user exists
764        let _target = ctx
765            .database
766            .get_user_by_id(&body.user_id)
767            .await?
768            .ok_or_else(|| AuthError::not_found("User not found"))?;
769
770        // Revoke all sessions first
771        ctx.database.delete_user_sessions(&body.user_id).await?;
772
773        // Delete all accounts linked to this user
774        let accounts = ctx.database.get_user_accounts(&body.user_id).await?;
775        for account in &accounts {
776            ctx.database.delete_account(account.id()).await?;
777        }
778
779        // Delete the user
780        ctx.database.delete_user(&body.user_id).await?;
781
782        let response = SuccessResponse { success: true };
783        AuthResponse::json(200, &response).map_err(AuthError::from)
784    }
785
786    /// POST /admin/set-user-password — Set a user's password.
787    async fn handle_set_user_password<DB: DatabaseAdapter>(
788        &self,
789        req: &AuthRequest,
790        ctx: &AuthContext<DB>,
791    ) -> AuthResult<AuthResponse> {
792        let (_admin_user, _admin_session) = self.require_admin(req, ctx).await?;
793
794        let body: SetUserPasswordRequest = match better_auth_core::validate_request_body(req) {
795            Ok(v) => v,
796            Err(resp) => return Ok(resp),
797        };
798
799        // Validate password length
800        if body.new_password.len() < ctx.config.password.min_length {
801            return Err(AuthError::bad_request(format!(
802                "Password must be at least {} characters long",
803                ctx.config.password.min_length
804            )));
805        }
806
807        // Verify user exists
808        let user = ctx
809            .database
810            .get_user_by_id(&body.user_id)
811            .await?
812            .ok_or_else(|| AuthError::not_found("User not found"))?;
813
814        let password_hash = better_auth_core::hash_password(None, &body.new_password).await?;
815
816        // Update password in user metadata
817        let mut metadata = user.metadata().clone();
818        if let Some(obj) = metadata.as_object_mut() {
819            obj.insert(
820                "password_hash".to_string(),
821                serde_json::json!(password_hash),
822            );
823        } else {
824            return Err(AuthError::bad_request(
825                "User metadata must be a JSON object to store password hash",
826            ));
827        }
828
829        let update = UpdateUser {
830            metadata: Some(metadata),
831            ..Default::default()
832        };
833        ctx.database.update_user(&body.user_id, update).await?;
834
835        // Update the credential account's password field (or create one if missing)
836        let accounts = ctx.database.get_user_accounts(&body.user_id).await?;
837        let has_credential = accounts.iter().any(|a| a.provider_id() == "credential");
838
839        if has_credential {
840            for account in &accounts {
841                if account.provider_id() == "credential" {
842                    let account_update = better_auth_core::UpdateAccount {
843                        password: Some(password_hash.clone()),
844                        ..Default::default()
845                    };
846                    ctx.database
847                        .update_account(account.id(), account_update)
848                        .await?;
849                    break;
850                }
851            }
852        } else {
853            // User has no credential account (e.g. OAuth-only user).
854            // Create one so the password is usable for email/password sign-in.
855            ctx.database
856                .create_account(CreateAccount {
857                    user_id: body.user_id.clone(),
858                    account_id: body.user_id.clone(),
859                    provider_id: "credential".to_string(),
860                    access_token: None,
861                    refresh_token: None,
862                    id_token: None,
863                    access_token_expires_at: None,
864                    refresh_token_expires_at: None,
865                    scope: None,
866                    password: Some(password_hash.clone()),
867                })
868                .await?;
869        }
870
871        let response = StatusResponse { status: true };
872        AuthResponse::json(200, &response).map_err(AuthError::from)
873    }
874
875    /// POST /admin/has-permission — Check if the calling user has a given permission.
876    async fn handle_has_permission<DB: DatabaseAdapter>(
877        &self,
878        req: &AuthRequest,
879        ctx: &AuthContext<DB>,
880    ) -> AuthResult<AuthResponse> {
881        let (user, _session) = ctx.require_session(req).await?;
882
883        let body: HasPermissionRequest = match better_auth_core::validate_request_body(req) {
884            Ok(v) => v,
885            Err(resp) => return Ok(resp),
886        };
887
888        // Use the `permissions` field, falling back to deprecated `permission`.
889        let _permissions = body.permissions.or(body.permission);
890
891        let is_admin = user.role().unwrap_or("user") == self.config.admin_role;
892
893        // If the user is an admin they have all permissions.
894        // Otherwise check if the requested permission matches a simple
895        // role-based scheme. The `permissions` object from the spec is
896        // free-form; we treat it as a map of resource -> action arrays
897        // and grant access if the user's role matches the admin role.
898        let (success, error) = if is_admin {
899            (true, None)
900        } else {
901            (
902                false,
903                Some("User does not have the required permissions".to_string()),
904            )
905        };
906
907        let response = PermissionResponse { success, error };
908        AuthResponse::json(200, &response).map_err(AuthError::from)
909    }
910}
911
912// ---------------------------------------------------------------------------
913// Tests
914// ---------------------------------------------------------------------------
915
916#[cfg(test)]
917mod tests {
918    use super::*;
919    use crate::plugins::test_helpers;
920    use better_auth_core::adapters::{AccountOps, MemoryDatabaseAdapter, SessionOps, UserOps};
921    use better_auth_core::entity::AuthAccount;
922    use better_auth_core::{CreateSession, Session, User};
923    use chrono::{Duration, Utc};
924    use std::collections::HashMap;
925    use std::sync::Arc;
926
927    async fn create_admin_context() -> (
928        AuthContext<MemoryDatabaseAdapter>,
929        User,
930        Session,
931        User,
932        Session,
933    ) {
934        let ctx = test_helpers::create_test_context();
935
936        // Create admin user
937        let admin = test_helpers::create_user(
938            &ctx,
939            CreateUser::new()
940                .with_email("admin@example.com")
941                .with_name("Admin")
942                .with_role("admin"),
943        )
944        .await;
945        let admin_session =
946            test_helpers::create_session(&ctx, admin.id.clone(), Duration::hours(24)).await;
947
948        // Create regular user
949        let user = test_helpers::create_user(
950            &ctx,
951            CreateUser::new()
952                .with_email("user@example.com")
953                .with_name("Regular User")
954                .with_role("user"),
955        )
956        .await;
957        let user_session =
958            test_helpers::create_session(&ctx, user.id.clone(), Duration::hours(24)).await;
959
960        (ctx, admin, admin_session, user, user_session)
961    }
962
963    fn make_request(
964        method: HttpMethod,
965        path: &str,
966        token: &str,
967        body: Option<serde_json::Value>,
968    ) -> AuthRequest {
969        test_helpers::create_auth_json_request_no_query(method, path, Some(token), body)
970    }
971
972    fn make_request_with_query(
973        method: HttpMethod,
974        path: &str,
975        token: &str,
976        body: Option<serde_json::Value>,
977        query: HashMap<String, String>,
978    ) -> AuthRequest {
979        test_helpers::create_auth_json_request(method, path, Some(token), body, query)
980    }
981
982    fn json_body(resp: &AuthResponse) -> serde_json::Value {
983        serde_json::from_slice(&resp.body).unwrap()
984    }
985
986    // -----------------------------------------------------------------------
987    // Basic auth / access control
988    // -----------------------------------------------------------------------
989
990    #[tokio::test]
991    async fn test_set_role() {
992        let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
993        let plugin = AdminPlugin::new();
994
995        let req = make_request(
996            HttpMethod::Post,
997            "/admin/set-role",
998            &admin_session.token,
999            Some(serde_json::json!({
1000                "userId": user.id,
1001                "role": "moderator"
1002            })),
1003        );
1004
1005        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1006        assert_eq!(resp.status, 200);
1007        let body = json_body(&resp);
1008        assert_eq!(body["user"]["role"], "moderator");
1009    }
1010
1011    #[tokio::test]
1012    async fn test_non_admin_rejected() {
1013        let (ctx, _admin, _admin_session, _user, user_session) = create_admin_context().await;
1014        let plugin = AdminPlugin::new();
1015
1016        let req = make_request(
1017            HttpMethod::Post,
1018            "/admin/set-role",
1019            &user_session.token,
1020            Some(serde_json::json!({
1021                "userId": "someone",
1022                "role": "admin"
1023            })),
1024        );
1025
1026        let result = plugin.on_request(&req, &ctx).await;
1027        assert!(result.is_err());
1028    }
1029
1030    #[tokio::test]
1031    async fn test_unauthenticated_rejected() {
1032        let (ctx, _admin, _admin_session, user, _user_session) = create_admin_context().await;
1033        let plugin = AdminPlugin::new();
1034
1035        let req = make_request(
1036            HttpMethod::Post,
1037            "/admin/set-role",
1038            "invalid-token",
1039            Some(serde_json::json!({
1040                "userId": user.id,
1041                "role": "admin"
1042            })),
1043        );
1044
1045        let result = plugin.on_request(&req, &ctx).await;
1046        assert!(result.is_err());
1047    }
1048
1049    #[tokio::test]
1050    async fn test_custom_admin_role() {
1051        let config = Arc::new(better_auth_core::AuthConfig::new(
1052            "test-secret-key-at-least-32-chars-long",
1053        ));
1054        let database = Arc::new(MemoryDatabaseAdapter::new());
1055        let ctx = AuthContext::new(config, database.clone());
1056
1057        // Create superadmin user with custom role
1058        let admin = database
1059            .create_user(
1060                CreateUser::new()
1061                    .with_email("superadmin@example.com")
1062                    .with_name("Super Admin")
1063                    .with_role("superadmin"),
1064            )
1065            .await
1066            .unwrap();
1067
1068        let admin_session = database
1069            .create_session(CreateSession {
1070                user_id: admin.id.clone(),
1071                expires_at: Utc::now() + Duration::hours(24),
1072                ip_address: None,
1073                user_agent: None,
1074                impersonated_by: None,
1075                active_organization_id: None,
1076            })
1077            .await
1078            .unwrap();
1079
1080        let user = database
1081            .create_user(
1082                CreateUser::new()
1083                    .with_email("user@example.com")
1084                    .with_name("User")
1085                    .with_role("user"),
1086            )
1087            .await
1088            .unwrap();
1089
1090        let plugin = AdminPlugin::new().admin_role("superadmin");
1091
1092        let req = make_request(
1093            HttpMethod::Post,
1094            "/admin/set-role",
1095            &admin_session.token,
1096            Some(serde_json::json!({
1097                "userId": user.id,
1098                "role": "moderator"
1099            })),
1100        );
1101
1102        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1103        assert_eq!(resp.status, 200);
1104        let body = json_body(&resp);
1105        assert_eq!(body["user"]["role"], "moderator");
1106    }
1107
1108    #[tokio::test]
1109    async fn test_non_admin_path_returns_none() {
1110        let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
1111        let plugin = AdminPlugin::new();
1112
1113        let req = make_request(
1114            HttpMethod::Post,
1115            "/api/not-admin",
1116            &admin_session.token,
1117            None,
1118        );
1119
1120        // Routes not matching /admin/* should return None (not handled by plugin)
1121        let result = plugin.on_request(&req, &ctx).await.unwrap();
1122        assert!(result.is_none());
1123    }
1124
1125    // -----------------------------------------------------------------------
1126    // Create user
1127    // -----------------------------------------------------------------------
1128
1129    #[tokio::test]
1130    async fn test_create_user() {
1131        let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
1132        let plugin = AdminPlugin::new();
1133
1134        let req = make_request(
1135            HttpMethod::Post,
1136            "/admin/create-user",
1137            &admin_session.token,
1138            Some(serde_json::json!({
1139                "email": "new@example.com",
1140                "password": "securepassword123",
1141                "name": "New User"
1142            })),
1143        );
1144
1145        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1146        assert_eq!(resp.status, 200);
1147        let body = json_body(&resp);
1148        assert_eq!(body["user"]["email"], "new@example.com");
1149        assert_eq!(body["user"]["name"], "New User");
1150        assert_eq!(body["user"]["role"], "user");
1151    }
1152
1153    #[tokio::test]
1154    async fn test_create_user_with_custom_role() {
1155        let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
1156        let plugin = AdminPlugin::new();
1157
1158        let req = make_request(
1159            HttpMethod::Post,
1160            "/admin/create-user",
1161            &admin_session.token,
1162            Some(serde_json::json!({
1163                "email": "mod@example.com",
1164                "password": "securepassword123",
1165                "name": "Moderator",
1166                "role": "moderator"
1167            })),
1168        );
1169
1170        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1171        assert_eq!(resp.status, 200);
1172        let body = json_body(&resp);
1173        assert_eq!(body["user"]["role"], "moderator");
1174    }
1175
1176    #[tokio::test]
1177    async fn test_create_user_creates_credential_account() {
1178        let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
1179        let plugin = AdminPlugin::new();
1180
1181        let req = make_request(
1182            HttpMethod::Post,
1183            "/admin/create-user",
1184            &admin_session.token,
1185            Some(serde_json::json!({
1186                "email": "new@example.com",
1187                "password": "securepassword123",
1188                "name": "New User"
1189            })),
1190        );
1191
1192        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1193        assert_eq!(resp.status, 200);
1194        let body = json_body(&resp);
1195        let user_id = body["user"]["id"].as_str().unwrap();
1196
1197        // Verify a credential account was created
1198        let accounts = ctx.database.get_user_accounts(user_id).await.unwrap();
1199        assert_eq!(accounts.len(), 1);
1200        assert_eq!(accounts[0].provider_id(), "credential");
1201        assert!(accounts[0].password().is_some());
1202    }
1203
1204    #[tokio::test]
1205    async fn test_create_user_duplicate_email_rejected() {
1206        let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
1207        let plugin = AdminPlugin::new();
1208
1209        // user@example.com already exists in the context
1210        let req = make_request(
1211            HttpMethod::Post,
1212            "/admin/create-user",
1213            &admin_session.token,
1214            Some(serde_json::json!({
1215                "email": "user@example.com",
1216                "password": "securepassword123",
1217                "name": "Duplicate"
1218            })),
1219        );
1220
1221        let result = plugin.on_request(&req, &ctx).await;
1222        assert!(result.is_err());
1223    }
1224
1225    #[tokio::test]
1226    async fn test_create_user_default_role_config() {
1227        let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
1228        let plugin = AdminPlugin::new().default_user_role("member");
1229
1230        let req = make_request(
1231            HttpMethod::Post,
1232            "/admin/create-user",
1233            &admin_session.token,
1234            Some(serde_json::json!({
1235                "email": "newmember@example.com",
1236                "password": "securepassword123",
1237                "name": "New Member"
1238            })),
1239        );
1240
1241        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1242        assert_eq!(resp.status, 200);
1243        let body = json_body(&resp);
1244        assert_eq!(body["user"]["role"], "member");
1245    }
1246
1247    // -----------------------------------------------------------------------
1248    // List users
1249    // -----------------------------------------------------------------------
1250
1251    #[tokio::test]
1252    async fn test_list_users() {
1253        let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
1254        let plugin = AdminPlugin::new();
1255
1256        let req = make_request(
1257            HttpMethod::Get,
1258            "/admin/list-users",
1259            &admin_session.token,
1260            None,
1261        );
1262
1263        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1264        assert_eq!(resp.status, 200);
1265        let body = json_body(&resp);
1266        assert_eq!(body["total"], 2); // admin + regular user
1267        assert_eq!(body["users"].as_array().unwrap().len(), 2);
1268    }
1269
1270    #[tokio::test]
1271    async fn test_list_users_pagination() {
1272        let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
1273        let plugin = AdminPlugin::new();
1274
1275        let mut query = HashMap::new();
1276        query.insert("limit".to_string(), "1".to_string());
1277        query.insert("offset".to_string(), "0".to_string());
1278
1279        let req = make_request_with_query(
1280            HttpMethod::Get,
1281            "/admin/list-users",
1282            &admin_session.token,
1283            None,
1284            query,
1285        );
1286
1287        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1288        assert_eq!(resp.status, 200);
1289        let body = json_body(&resp);
1290        assert_eq!(body["total"], 2); // total is still 2
1291        assert_eq!(body["users"].as_array().unwrap().len(), 1); // but only 1 returned
1292        assert_eq!(body["limit"], 1);
1293        assert_eq!(body["offset"], 0);
1294    }
1295
1296    #[tokio::test]
1297    async fn test_list_users_respects_max_page_limit() {
1298        let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
1299        let plugin = AdminPlugin::new().max_page_limit(1);
1300
1301        // Request limit=100 but max is 1
1302        let mut query = HashMap::new();
1303        query.insert("limit".to_string(), "100".to_string());
1304
1305        let req = make_request_with_query(
1306            HttpMethod::Get,
1307            "/admin/list-users",
1308            &admin_session.token,
1309            None,
1310            query,
1311        );
1312
1313        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1314        assert_eq!(resp.status, 200);
1315        let body = json_body(&resp);
1316        // Should be clamped to max_page_limit=1
1317        assert_eq!(body["users"].as_array().unwrap().len(), 1);
1318        assert_eq!(body["limit"], 1);
1319    }
1320
1321    // -----------------------------------------------------------------------
1322    // List user sessions
1323    // -----------------------------------------------------------------------
1324
1325    #[tokio::test]
1326    async fn test_list_user_sessions() {
1327        let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
1328        let plugin = AdminPlugin::new();
1329
1330        let req = make_request(
1331            HttpMethod::Post,
1332            "/admin/list-user-sessions",
1333            &admin_session.token,
1334            Some(serde_json::json!({
1335                "userId": user.id,
1336            })),
1337        );
1338
1339        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1340        assert_eq!(resp.status, 200);
1341        let body = json_body(&resp);
1342        let sessions = body["sessions"].as_array().unwrap();
1343        assert_eq!(sessions.len(), 1);
1344    }
1345
1346    #[tokio::test]
1347    async fn test_list_user_sessions_nonexistent_user() {
1348        let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
1349        let plugin = AdminPlugin::new();
1350
1351        let req = make_request(
1352            HttpMethod::Post,
1353            "/admin/list-user-sessions",
1354            &admin_session.token,
1355            Some(serde_json::json!({
1356                "userId": "nonexistent-id",
1357            })),
1358        );
1359
1360        let result = plugin.on_request(&req, &ctx).await;
1361        assert!(result.is_err());
1362    }
1363
1364    // -----------------------------------------------------------------------
1365    // Ban / Unban
1366    // -----------------------------------------------------------------------
1367
1368    #[tokio::test]
1369    async fn test_ban_unban_user() {
1370        let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
1371        let plugin = AdminPlugin::new();
1372
1373        // Ban user
1374        let req = make_request(
1375            HttpMethod::Post,
1376            "/admin/ban-user",
1377            &admin_session.token,
1378            Some(serde_json::json!({
1379                "userId": user.id,
1380                "banReason": "spam"
1381            })),
1382        );
1383
1384        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1385        assert_eq!(resp.status, 200);
1386        let body = json_body(&resp);
1387        assert_eq!(body["user"]["banned"], true);
1388
1389        // Unban user
1390        let req = make_request(
1391            HttpMethod::Post,
1392            "/admin/unban-user",
1393            &admin_session.token,
1394            Some(serde_json::json!({
1395                "userId": user.id,
1396            })),
1397        );
1398
1399        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1400        assert_eq!(resp.status, 200);
1401        let body = json_body(&resp);
1402        assert_eq!(body["user"]["banned"], false);
1403    }
1404
1405    #[tokio::test]
1406    async fn test_cannot_ban_self() {
1407        let (ctx, admin, admin_session, _user, _user_session) = create_admin_context().await;
1408        let plugin = AdminPlugin::new();
1409
1410        let req = make_request(
1411            HttpMethod::Post,
1412            "/admin/ban-user",
1413            &admin_session.token,
1414            Some(serde_json::json!({
1415                "userId": admin.id,
1416            })),
1417        );
1418
1419        let result = plugin.on_request(&req, &ctx).await;
1420        assert!(result.is_err());
1421    }
1422
1423    /// Verifies the bug fix: unbanning clears ban_reason and ban_expires in the adapter.
1424    #[tokio::test]
1425    async fn test_unban_clears_ban_reason_and_expires() {
1426        let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
1427        let plugin = AdminPlugin::new();
1428
1429        // Ban with reason and expiry
1430        let req = make_request(
1431            HttpMethod::Post,
1432            "/admin/ban-user",
1433            &admin_session.token,
1434            Some(serde_json::json!({
1435                "userId": user.id,
1436                "banReason": "spam",
1437                "banExpiresIn": 3600
1438            })),
1439        );
1440
1441        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1442        assert_eq!(resp.status, 200);
1443        let body = json_body(&resp);
1444        assert_eq!(body["user"]["banned"], true);
1445        assert_eq!(body["user"]["banReason"], "spam");
1446        assert!(!body["user"]["banExpires"].is_null());
1447
1448        // Unban
1449        let req = make_request(
1450            HttpMethod::Post,
1451            "/admin/unban-user",
1452            &admin_session.token,
1453            Some(serde_json::json!({
1454                "userId": user.id,
1455            })),
1456        );
1457
1458        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1459        assert_eq!(resp.status, 200);
1460        let body = json_body(&resp);
1461        assert_eq!(body["user"]["banned"], false);
1462
1463        // Verify ban_reason and ban_expires are cleared by checking the DB directly
1464        let updated_user = ctx
1465            .database
1466            .get_user_by_id(&user.id)
1467            .await
1468            .unwrap()
1469            .unwrap();
1470        assert!(!updated_user.banned);
1471        assert!(updated_user.ban_reason.is_none());
1472        assert!(updated_user.ban_expires.is_none());
1473    }
1474
1475    #[tokio::test]
1476    async fn test_ban_with_expiry() {
1477        let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
1478        let plugin = AdminPlugin::new();
1479
1480        let req = make_request(
1481            HttpMethod::Post,
1482            "/admin/ban-user",
1483            &admin_session.token,
1484            Some(serde_json::json!({
1485                "userId": user.id,
1486                "banExpiresIn": 7200 // 2 hours
1487            })),
1488        );
1489
1490        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1491        assert_eq!(resp.status, 200);
1492        let body = json_body(&resp);
1493        assert_eq!(body["user"]["banned"], true);
1494        assert!(!body["user"]["banExpires"].is_null());
1495    }
1496
1497    #[tokio::test]
1498    async fn test_ban_revokes_user_sessions() {
1499        let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
1500        let plugin = AdminPlugin::new();
1501
1502        // Confirm user has sessions
1503        let sessions = ctx.database.get_user_sessions(&user.id).await.unwrap();
1504        assert!(!sessions.is_empty());
1505
1506        // Ban user
1507        let req = make_request(
1508            HttpMethod::Post,
1509            "/admin/ban-user",
1510            &admin_session.token,
1511            Some(serde_json::json!({
1512                "userId": user.id,
1513                "banReason": "bad behavior"
1514            })),
1515        );
1516
1517        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1518        assert_eq!(resp.status, 200);
1519
1520        // After ban, sessions should be revoked
1521        let sessions = ctx.database.get_user_sessions(&user.id).await.unwrap();
1522        assert!(sessions.is_empty());
1523    }
1524
1525    #[tokio::test]
1526    async fn test_cannot_ban_admin_by_default() {
1527        let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
1528        let plugin = AdminPlugin::new();
1529
1530        // Create second admin
1531        let admin2 = ctx
1532            .database
1533            .create_user(
1534                CreateUser::new()
1535                    .with_email("admin2@example.com")
1536                    .with_name("Admin 2")
1537                    .with_role("admin"),
1538            )
1539            .await
1540            .unwrap();
1541
1542        let req = make_request(
1543            HttpMethod::Post,
1544            "/admin/ban-user",
1545            &admin_session.token,
1546            Some(serde_json::json!({
1547                "userId": admin2.id,
1548            })),
1549        );
1550
1551        let result = plugin.on_request(&req, &ctx).await;
1552        assert!(result.is_err());
1553    }
1554
1555    #[tokio::test]
1556    async fn test_allow_ban_admin_config() {
1557        let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
1558        let plugin = AdminPlugin::new().allow_ban_admin(true);
1559
1560        // Create second admin
1561        let admin2 = ctx
1562            .database
1563            .create_user(
1564                CreateUser::new()
1565                    .with_email("admin2@example.com")
1566                    .with_name("Admin 2")
1567                    .with_role("admin"),
1568            )
1569            .await
1570            .unwrap();
1571
1572        let req = make_request(
1573            HttpMethod::Post,
1574            "/admin/ban-user",
1575            &admin_session.token,
1576            Some(serde_json::json!({
1577                "userId": admin2.id,
1578            })),
1579        );
1580
1581        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1582        assert_eq!(resp.status, 200);
1583        let body = json_body(&resp);
1584        assert_eq!(body["user"]["banned"], true);
1585    }
1586
1587    // -----------------------------------------------------------------------
1588    // Impersonation
1589    // -----------------------------------------------------------------------
1590
1591    #[tokio::test]
1592    async fn test_impersonate_and_stop() {
1593        let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
1594        let plugin = AdminPlugin::new();
1595
1596        // Impersonate
1597        let req = make_request(
1598            HttpMethod::Post,
1599            "/admin/impersonate-user",
1600            &admin_session.token,
1601            Some(serde_json::json!({
1602                "userId": user.id,
1603            })),
1604        );
1605
1606        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1607        assert_eq!(resp.status, 200);
1608        let body = json_body(&resp);
1609        assert_eq!(body["user"]["email"], "user@example.com");
1610        assert!(body["session"]["token"].is_string());
1611    }
1612
1613    /// Verifies the impersonation session has the impersonated_by field set.
1614    #[tokio::test]
1615    async fn test_impersonate_session_has_impersonated_by() {
1616        let (ctx, admin, admin_session, user, _user_session) = create_admin_context().await;
1617        let plugin = AdminPlugin::new();
1618
1619        let req = make_request(
1620            HttpMethod::Post,
1621            "/admin/impersonate-user",
1622            &admin_session.token,
1623            Some(serde_json::json!({
1624                "userId": user.id,
1625            })),
1626        );
1627
1628        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1629        assert_eq!(resp.status, 200);
1630        let body = json_body(&resp);
1631        let imp_token = body["session"]["token"].as_str().unwrap();
1632
1633        // Look up the impersonation session and check impersonated_by
1634        let imp_session = ctx.database.get_session(imp_token).await.unwrap().unwrap();
1635        assert_eq!(
1636            imp_session.impersonated_by().unwrap(),
1637            admin.id,
1638            "impersonated_by should be the admin's user id"
1639        );
1640    }
1641
1642    /// Verifies the bug fix: stop-impersonating creates a new admin session.
1643    #[tokio::test]
1644    async fn test_stop_impersonating_creates_admin_session() {
1645        let (ctx, admin, admin_session, user, _user_session) = create_admin_context().await;
1646        let plugin = AdminPlugin::new();
1647
1648        // Impersonate
1649        let req = make_request(
1650            HttpMethod::Post,
1651            "/admin/impersonate-user",
1652            &admin_session.token,
1653            Some(serde_json::json!({
1654                "userId": user.id,
1655            })),
1656        );
1657
1658        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1659        let body = json_body(&resp);
1660        let imp_token = body["session"]["token"].as_str().unwrap().to_string();
1661
1662        // Stop impersonating using the impersonation session token
1663        let req = make_request(
1664            HttpMethod::Post,
1665            "/admin/stop-impersonating",
1666            &imp_token,
1667            None,
1668        );
1669
1670        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1671        assert_eq!(resp.status, 200);
1672        let body = json_body(&resp);
1673
1674        // Should return admin user and a new session
1675        assert_eq!(body["user"]["email"], "admin@example.com");
1676        assert!(body["session"]["token"].is_string());
1677
1678        // The new session token should be for the admin
1679        let new_token = body["session"]["token"].as_str().unwrap();
1680        let new_session = ctx.database.get_session(new_token).await.unwrap().unwrap();
1681        assert_eq!(new_session.user_id, admin.id);
1682        assert!(
1683            new_session.impersonated_by.is_none(),
1684            "new admin session should not be an impersonation session"
1685        );
1686
1687        // The response should include a Set-Cookie header
1688        assert!(resp.headers.contains_key("Set-Cookie"));
1689
1690        // The old impersonation session should be deleted
1691        let old_session = ctx.database.get_session(&imp_token).await.unwrap();
1692        assert!(old_session.is_none());
1693    }
1694
1695    #[tokio::test]
1696    async fn test_stop_impersonating_non_impersonation_session_rejected() {
1697        let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
1698        let plugin = AdminPlugin::new();
1699
1700        // Try to stop impersonating with a normal admin session (not impersonation)
1701        let req = make_request(
1702            HttpMethod::Post,
1703            "/admin/stop-impersonating",
1704            &admin_session.token,
1705            None,
1706        );
1707
1708        let result = plugin.on_request(&req, &ctx).await;
1709        assert!(result.is_err());
1710    }
1711
1712    #[tokio::test]
1713    async fn test_cannot_impersonate_self() {
1714        let (ctx, admin, admin_session, _user, _user_session) = create_admin_context().await;
1715        let plugin = AdminPlugin::new();
1716
1717        let req = make_request(
1718            HttpMethod::Post,
1719            "/admin/impersonate-user",
1720            &admin_session.token,
1721            Some(serde_json::json!({
1722                "userId": admin.id,
1723            })),
1724        );
1725
1726        let result = plugin.on_request(&req, &ctx).await;
1727        assert!(result.is_err());
1728    }
1729
1730    #[tokio::test]
1731    async fn test_impersonate_nonexistent_user_rejected() {
1732        let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
1733        let plugin = AdminPlugin::new();
1734
1735        let req = make_request(
1736            HttpMethod::Post,
1737            "/admin/impersonate-user",
1738            &admin_session.token,
1739            Some(serde_json::json!({
1740                "userId": "nonexistent-user-id",
1741            })),
1742        );
1743
1744        let result = plugin.on_request(&req, &ctx).await;
1745        assert!(result.is_err());
1746    }
1747
1748    #[tokio::test]
1749    async fn test_impersonate_response_has_set_cookie() {
1750        let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
1751        let plugin = AdminPlugin::new();
1752
1753        let req = make_request(
1754            HttpMethod::Post,
1755            "/admin/impersonate-user",
1756            &admin_session.token,
1757            Some(serde_json::json!({
1758                "userId": user.id,
1759            })),
1760        );
1761
1762        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1763        assert_eq!(resp.status, 200);
1764        assert!(
1765            resp.headers.contains_key("Set-Cookie"),
1766            "impersonate response should set a session cookie"
1767        );
1768    }
1769
1770    // -----------------------------------------------------------------------
1771    // Session management
1772    // -----------------------------------------------------------------------
1773
1774    #[tokio::test]
1775    async fn test_revoke_user_sessions() {
1776        let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
1777        let plugin = AdminPlugin::new();
1778
1779        let req = make_request(
1780            HttpMethod::Post,
1781            "/admin/revoke-user-sessions",
1782            &admin_session.token,
1783            Some(serde_json::json!({
1784                "userId": user.id,
1785            })),
1786        );
1787
1788        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1789        assert_eq!(resp.status, 200);
1790        let body = json_body(&resp);
1791        assert_eq!(body["success"], true);
1792
1793        // Verify sessions are actually deleted
1794        let sessions = ctx.database.get_user_sessions(&user.id).await.unwrap();
1795        assert!(sessions.is_empty());
1796    }
1797
1798    #[tokio::test]
1799    async fn test_revoke_specific_session() {
1800        let (ctx, _admin, admin_session, _user, user_session) = create_admin_context().await;
1801        let plugin = AdminPlugin::new();
1802
1803        let req = make_request(
1804            HttpMethod::Post,
1805            "/admin/revoke-user-session",
1806            &admin_session.token,
1807            Some(serde_json::json!({
1808                "sessionToken": user_session.token,
1809            })),
1810        );
1811
1812        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1813        assert_eq!(resp.status, 200);
1814        let body = json_body(&resp);
1815        assert_eq!(body["success"], true);
1816
1817        // Verify specific session is deleted
1818        let session = ctx.database.get_session(&user_session.token).await.unwrap();
1819        assert!(session.is_none());
1820    }
1821
1822    // -----------------------------------------------------------------------
1823    // Remove user
1824    // -----------------------------------------------------------------------
1825
1826    #[tokio::test]
1827    async fn test_remove_user() {
1828        let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
1829        let plugin = AdminPlugin::new();
1830
1831        let req = make_request(
1832            HttpMethod::Post,
1833            "/admin/remove-user",
1834            &admin_session.token,
1835            Some(serde_json::json!({
1836                "userId": user.id,
1837            })),
1838        );
1839
1840        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1841        assert_eq!(resp.status, 200);
1842        let body = json_body(&resp);
1843        assert_eq!(body["success"], true);
1844
1845        // Verify user is deleted
1846        let deleted = ctx.database.get_user_by_id(&user.id).await.unwrap();
1847        assert!(deleted.is_none());
1848    }
1849
1850    #[tokio::test]
1851    async fn test_cannot_remove_self() {
1852        let (ctx, admin, admin_session, _user, _user_session) = create_admin_context().await;
1853        let plugin = AdminPlugin::new();
1854
1855        let req = make_request(
1856            HttpMethod::Post,
1857            "/admin/remove-user",
1858            &admin_session.token,
1859            Some(serde_json::json!({
1860                "userId": admin.id,
1861            })),
1862        );
1863
1864        let result = plugin.on_request(&req, &ctx).await;
1865        assert!(result.is_err());
1866    }
1867
1868    #[tokio::test]
1869    async fn test_remove_user_cleans_up_sessions_and_accounts() {
1870        let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
1871        let plugin = AdminPlugin::new();
1872
1873        // First create a user with an account
1874        let req = make_request(
1875            HttpMethod::Post,
1876            "/admin/create-user",
1877            &admin_session.token,
1878            Some(serde_json::json!({
1879                "email": "tobedeleted@example.com",
1880                "password": "securepassword123",
1881                "name": "To Be Deleted"
1882            })),
1883        );
1884        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1885        let body = json_body(&resp);
1886        let user_id = body["user"]["id"].as_str().unwrap().to_string();
1887
1888        // Verify user has an account
1889        let accounts = ctx.database.get_user_accounts(&user_id).await.unwrap();
1890        assert_eq!(accounts.len(), 1);
1891
1892        // Remove the user
1893        let req = make_request(
1894            HttpMethod::Post,
1895            "/admin/remove-user",
1896            &admin_session.token,
1897            Some(serde_json::json!({
1898                "userId": user_id,
1899            })),
1900        );
1901        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1902        assert_eq!(resp.status, 200);
1903
1904        // Verify user is deleted
1905        let deleted = ctx.database.get_user_by_id(&user_id).await.unwrap();
1906        assert!(deleted.is_none());
1907
1908        // Verify accounts are cleaned up
1909        let accounts = ctx.database.get_user_accounts(&user_id).await.unwrap();
1910        assert!(accounts.is_empty());
1911    }
1912
1913    #[tokio::test]
1914    async fn test_remove_nonexistent_user() {
1915        let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
1916        let plugin = AdminPlugin::new();
1917
1918        let req = make_request(
1919            HttpMethod::Post,
1920            "/admin/remove-user",
1921            &admin_session.token,
1922            Some(serde_json::json!({
1923                "userId": "nonexistent-user-id",
1924            })),
1925        );
1926
1927        let result = plugin.on_request(&req, &ctx).await;
1928        assert!(result.is_err());
1929    }
1930
1931    // -----------------------------------------------------------------------
1932    // Set user password
1933    // -----------------------------------------------------------------------
1934
1935    #[tokio::test]
1936    async fn test_set_user_password() {
1937        let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
1938        let plugin = AdminPlugin::new();
1939
1940        // First create a user with a credential account
1941        let req = make_request(
1942            HttpMethod::Post,
1943            "/admin/create-user",
1944            &admin_session.token,
1945            Some(serde_json::json!({
1946                "email": "pwuser@example.com",
1947                "password": "oldpassword123",
1948                "name": "PW User"
1949            })),
1950        );
1951        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1952        let body = json_body(&resp);
1953        let user_id = body["user"]["id"].as_str().unwrap().to_string();
1954
1955        // Set new password
1956        let req = make_request(
1957            HttpMethod::Post,
1958            "/admin/set-user-password",
1959            &admin_session.token,
1960            Some(serde_json::json!({
1961                "userId": user_id,
1962                "newPassword": "newpassword456"
1963            })),
1964        );
1965
1966        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1967        assert_eq!(resp.status, 200);
1968        let body = json_body(&resp);
1969        assert_eq!(body["status"], true);
1970    }
1971
1972    /// Verifies the bug fix: set-user-password also updates the credential account password.
1973    #[tokio::test]
1974    async fn test_set_user_password_updates_account() {
1975        let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
1976        let plugin = AdminPlugin::new();
1977
1978        // First create a user with a credential account
1979        let req = make_request(
1980            HttpMethod::Post,
1981            "/admin/create-user",
1982            &admin_session.token,
1983            Some(serde_json::json!({
1984                "email": "pwuser@example.com",
1985                "password": "oldpassword123",
1986                "name": "PW User"
1987            })),
1988        );
1989        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1990        let body = json_body(&resp);
1991        let user_id = body["user"]["id"].as_str().unwrap().to_string();
1992
1993        // Get the old password hash from the credential account
1994        let accounts_before = ctx.database.get_user_accounts(&user_id).await.unwrap();
1995        let old_password = accounts_before[0].password().unwrap().to_string();
1996
1997        // Set new password
1998        let req = make_request(
1999            HttpMethod::Post,
2000            "/admin/set-user-password",
2001            &admin_session.token,
2002            Some(serde_json::json!({
2003                "userId": user_id,
2004                "newPassword": "newpassword456"
2005            })),
2006        );
2007        plugin.on_request(&req, &ctx).await.unwrap().unwrap();
2008
2009        // Verify the credential account password was updated
2010        let accounts_after = ctx.database.get_user_accounts(&user_id).await.unwrap();
2011        let new_password = accounts_after[0].password().unwrap().to_string();
2012        assert_ne!(
2013            old_password, new_password,
2014            "credential account password should be updated"
2015        );
2016    }
2017
2018    #[tokio::test]
2019    async fn test_set_user_password_too_short() {
2020        let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
2021        let plugin = AdminPlugin::new();
2022
2023        let req = make_request(
2024            HttpMethod::Post,
2025            "/admin/set-user-password",
2026            &admin_session.token,
2027            Some(serde_json::json!({
2028                "userId": user.id,
2029                "newPassword": "ab" // too short
2030            })),
2031        );
2032
2033        let result = plugin.on_request(&req, &ctx).await;
2034        assert!(result.is_err());
2035    }
2036
2037    #[tokio::test]
2038    async fn test_set_password_nonexistent_user() {
2039        let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
2040        let plugin = AdminPlugin::new();
2041
2042        let req = make_request(
2043            HttpMethod::Post,
2044            "/admin/set-user-password",
2045            &admin_session.token,
2046            Some(serde_json::json!({
2047                "userId": "nonexistent-user-id",
2048                "newPassword": "newpassword456"
2049            })),
2050        );
2051
2052        let result = plugin.on_request(&req, &ctx).await;
2053        assert!(result.is_err());
2054    }
2055
2056    // -----------------------------------------------------------------------
2057    // Permissions
2058    // -----------------------------------------------------------------------
2059
2060    #[tokio::test]
2061    async fn test_has_permission_admin() {
2062        let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
2063        let plugin = AdminPlugin::new();
2064
2065        let req = make_request(
2066            HttpMethod::Post,
2067            "/admin/has-permission",
2068            &admin_session.token,
2069            Some(serde_json::json!({
2070                "permissions": { "users": ["read", "write"] }
2071            })),
2072        );
2073
2074        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
2075        assert_eq!(resp.status, 200);
2076        let body = json_body(&resp);
2077        assert_eq!(body["success"], true);
2078    }
2079
2080    #[tokio::test]
2081    async fn test_has_permission_non_admin() {
2082        let (ctx, _admin, _admin_session, _user, user_session) = create_admin_context().await;
2083        let plugin = AdminPlugin::new();
2084
2085        let req = make_request(
2086            HttpMethod::Post,
2087            "/admin/has-permission",
2088            &user_session.token,
2089            Some(serde_json::json!({
2090                "permissions": { "users": ["read", "write"] }
2091            })),
2092        );
2093
2094        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
2095        assert_eq!(resp.status, 200);
2096        let body = json_body(&resp);
2097        assert_eq!(body["success"], false);
2098        assert!(body["error"].is_string());
2099    }
2100
2101    // -----------------------------------------------------------------------
2102    // Set role edge cases
2103    // -----------------------------------------------------------------------
2104
2105    #[tokio::test]
2106    async fn test_set_role_nonexistent_user() {
2107        let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
2108        let plugin = AdminPlugin::new();
2109
2110        let req = make_request(
2111            HttpMethod::Post,
2112            "/admin/set-role",
2113            &admin_session.token,
2114            Some(serde_json::json!({
2115                "userId": "nonexistent-user-id",
2116                "role": "admin"
2117            })),
2118        );
2119
2120        let result = plugin.on_request(&req, &ctx).await;
2121        assert!(result.is_err());
2122    }
2123
2124    #[tokio::test]
2125    async fn test_set_role_persists_in_database() {
2126        let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
2127        let plugin = AdminPlugin::new();
2128
2129        let req = make_request(
2130            HttpMethod::Post,
2131            "/admin/set-role",
2132            &admin_session.token,
2133            Some(serde_json::json!({
2134                "userId": user.id,
2135                "role": "editor"
2136            })),
2137        );
2138
2139        plugin.on_request(&req, &ctx).await.unwrap().unwrap();
2140
2141        // Verify role is persisted in the database
2142        let updated = ctx
2143            .database
2144            .get_user_by_id(&user.id)
2145            .await
2146            .unwrap()
2147            .unwrap();
2148        assert_eq!(updated.role.as_deref(), Some("editor"));
2149    }
2150
2151    // -----------------------------------------------------------------------
2152    // Plugin name / routes
2153    // -----------------------------------------------------------------------
2154
2155    #[tokio::test]
2156    async fn test_plugin_name() {
2157        let plugin = AdminPlugin::new();
2158        assert_eq!(
2159            <AdminPlugin as AuthPlugin<MemoryDatabaseAdapter>>::name(&plugin),
2160            "admin"
2161        );
2162    }
2163
2164    #[tokio::test]
2165    async fn test_plugin_routes_count() {
2166        let plugin = AdminPlugin::new();
2167        let routes = <AdminPlugin as AuthPlugin<MemoryDatabaseAdapter>>::routes(&plugin);
2168        assert_eq!(routes.len(), 13, "admin plugin should register 13 routes");
2169    }
2170}