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
16pub struct AdminPlugin {
28 config: AdminConfig,
29}
30
31#[derive(Debug, Clone)]
33pub struct AdminConfig {
34 pub admin_role: String,
36 pub default_user_role: String,
38 pub allow_ban_admin: bool,
40 pub default_page_limit: usize,
42 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#[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 #[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#[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#[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
291impl AdminPlugin {
296 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 session_manager.delete_session(&token).await?;
741
742 let admin_user = ctx
744 .database
745 .get_user_by_id(&admin_id)
746 .await?
747 .ok_or(AuthError::UserNotFound)?;
748
749 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 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 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 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 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 if body.user_id == admin_user.id() {
842 return Err(AuthError::bad_request("You cannot remove yourself"));
843 }
844
845 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 ctx.database.delete_user_sessions(&body.user_id).await?;
854
855 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 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 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 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 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 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 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 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 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 let _permissions = body.permissions.or(body.permission);
997
998 let is_admin = user.role().unwrap_or("user") == self.config.admin_role;
999
1000 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#[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 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 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 #[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 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 let result = plugin.on_request(&req, &ctx).await.unwrap();
1272 assert!(result.is_none());
1273 }
1274
1275 #[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 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 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 #[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); 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); assert_eq!(body["users"].as_array().unwrap().len(), 1); 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 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 assert_eq!(body["users"].as_array().unwrap().len(), 1);
1468 assert_eq!(body["limit"], 1);
1469 }
1470
1471 #[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 #[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 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 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 #[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 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 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 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 })),
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 let sessions = ctx.database.get_user_sessions(&user.id).await.unwrap();
1654 assert!(!sessions.is_empty());
1655
1656 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 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 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 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 #[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 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 #[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 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 #[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 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 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 assert_eq!(body["user"]["email"], "admin@example.com");
1826 assert!(body["session"]["token"].is_string());
1827
1828 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 assert!(resp.headers.contains_key("Set-Cookie"));
1839
1840 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 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 #[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 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 let session = ctx.database.get_session(&user_session.token).await.unwrap();
1969 assert!(session.is_none());
1970 }
1971
1972 #[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 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 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 let accounts = ctx.database.get_user_accounts(&user_id).await.unwrap();
2040 assert_eq!(accounts.len(), 1);
2041
2042 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 let deleted = ctx.database.get_user_by_id(&user_id).await.unwrap();
2056 assert!(deleted.is_none());
2057
2058 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 #[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 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 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 #[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 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 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 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 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" })),
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 #[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 #[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 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 #[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}