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