Skip to main content

better_auth_api/plugins/
admin.rs

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