1use chrono::Utc;
2use rand::Rng;
3use tracing::instrument;
4use uuid::Uuid;
5
6use authx_core::{
7 crypto::sha256_hex,
8 error::{AuthError, Result},
9 events::{AuthEvent, EventBus},
10 models::{CreateSession, CreateUser, Session, UpdateUser, User},
11};
12use authx_storage::ports::{AuditLogRepository, OrgRepository, SessionRepository, UserRepository};
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum BanStatus {
16 Banned,
17 Active,
18}
19
20pub struct AdminService<S> {
25 storage: S,
26 events: EventBus,
27 session_ttl_secs: i64,
28}
29
30impl<S> AdminService<S>
31where
32 S: UserRepository
33 + SessionRepository
34 + OrgRepository
35 + AuditLogRepository
36 + Clone
37 + Send
38 + Sync
39 + 'static,
40{
41 pub fn new(storage: S, events: EventBus, session_ttl_secs: i64) -> Self {
42 Self {
43 storage,
44 events,
45 session_ttl_secs,
46 }
47 }
48
49 #[instrument(skip(self))]
51 pub async fn list_users(&self, offset: u32, limit: u32) -> Result<Vec<User>> {
52 let users = UserRepository::list(&self.storage, offset, limit).await?;
53 tracing::debug!(offset, limit, count = users.len(), "admin: users listed");
54 Ok(users)
55 }
56
57 #[instrument(skip(self), fields(target = %user_id))]
59 pub async fn get_user(&self, user_id: Uuid) -> Result<User> {
60 UserRepository::find_by_id(&self.storage, user_id)
61 .await?
62 .ok_or(AuthError::UserNotFound)
63 }
64
65 #[instrument(skip(self), fields(acting_admin = %admin_id, email = %email))]
67 pub async fn create_user(&self, admin_id: Uuid, email: String) -> Result<User> {
68 let user = UserRepository::create(
69 &self.storage,
70 CreateUser {
71 email: email.clone(),
72 username: None,
73 metadata: None,
74 },
75 )
76 .await?;
77
78 AuditLogRepository::append(
79 &self.storage,
80 authx_core::models::CreateAuditLog {
81 user_id: Some(admin_id),
82 org_id: None,
83 action: "admin.create_user".into(),
84 resource_type: "user".into(),
85 resource_id: Some(user.id.to_string()),
86 ip_address: None,
87 metadata: Some(serde_json::json!({ "email": email })),
88 },
89 )
90 .await?;
91
92 self.events
93 .emit(AuthEvent::UserCreated { user: user.clone() });
94 tracing::info!(admin = %admin_id, user_id = %user.id, "admin: user created");
95 Ok(user)
96 }
97
98 #[instrument(skip(self), fields(acting_admin = %admin_id, org_id = %org_id, user_id = %user_id))]
100 pub async fn set_role(
101 &self,
102 admin_id: Uuid,
103 org_id: Uuid,
104 user_id: Uuid,
105 role_id: Uuid,
106 ) -> Result<authx_core::models::Membership> {
107 let membership =
108 OrgRepository::update_member_role(&self.storage, org_id, user_id, role_id).await?;
109
110 AuditLogRepository::append(
111 &self.storage,
112 authx_core::models::CreateAuditLog {
113 user_id: Some(admin_id),
114 org_id: Some(org_id),
115 action: "admin.set_role".into(),
116 resource_type: "membership".into(),
117 resource_id: Some(user_id.to_string()),
118 ip_address: None,
119 metadata: Some(serde_json::json!({ "role_id": role_id })),
120 },
121 )
122 .await?;
123
124 tracing::info!(admin = %admin_id, org_id = %org_id, user_id = %user_id, role_id = %role_id, "admin: role set");
125 Ok(membership)
126 }
127
128 #[instrument(skip(self), fields(target = %user_id, acting_admin = %admin_id))]
130 pub async fn ban_user(&self, admin_id: Uuid, user_id: Uuid, reason: &str) -> Result<()> {
131 UserRepository::update(
132 &self.storage,
133 user_id,
134 UpdateUser {
135 metadata: Some(serde_json::json!({ "banned": true, "ban_reason": reason })),
136 ..Default::default()
137 },
138 )
139 .await?;
140
141 SessionRepository::invalidate_all_for_user(&self.storage, user_id).await?;
142
143 AuditLogRepository::append(
144 &self.storage,
145 authx_core::models::CreateAuditLog {
146 user_id: Some(admin_id),
147 org_id: None,
148 action: "admin.ban_user".into(),
149 resource_type: "user".into(),
150 resource_id: Some(user_id.to_string()),
151 ip_address: None,
152 metadata: Some(serde_json::json!({ "reason": reason })),
153 },
154 )
155 .await?;
156
157 tracing::info!(admin = %admin_id, target = %user_id, reason, "user banned");
158 Ok(())
159 }
160
161 #[instrument(skip(self), fields(target = %user_id, acting_admin = %admin_id))]
163 pub async fn unban_user(&self, admin_id: Uuid, user_id: Uuid) -> Result<()> {
164 UserRepository::update(
165 &self.storage,
166 user_id,
167 UpdateUser {
168 metadata: Some(serde_json::json!({ "banned": false })),
169 ..Default::default()
170 },
171 )
172 .await?;
173
174 AuditLogRepository::append(
175 &self.storage,
176 authx_core::models::CreateAuditLog {
177 user_id: Some(admin_id),
178 org_id: None,
179 action: "admin.unban_user".into(),
180 resource_type: "user".into(),
181 resource_id: Some(user_id.to_string()),
182 ip_address: None,
183 metadata: None,
184 },
185 )
186 .await?;
187
188 tracing::info!(admin = %admin_id, target = %user_id, "user unbanned");
189 Ok(())
190 }
191
192 pub async fn ban_status(&self, user_id: Uuid) -> Result<BanStatus> {
193 let user = UserRepository::find_by_id(&self.storage, user_id)
194 .await?
195 .ok_or(AuthError::UserNotFound)?;
196 let banned = user
197 .metadata
198 .get("banned")
199 .and_then(|v| v.as_bool())
200 .unwrap_or(false);
201 Ok(if banned {
202 BanStatus::Banned
203 } else {
204 BanStatus::Active
205 })
206 }
207
208 #[instrument(skip(self), fields(target = %target_id, acting_admin = %admin_id))]
210 pub async fn impersonate(
211 &self,
212 admin_id: Uuid,
213 target_id: Uuid,
214 admin_ip: &str,
215 ) -> Result<(Session, String)> {
216 let raw: [u8; 32] = rand::thread_rng().gen();
217 let raw_token = hex::encode(raw);
218 let token_hash = sha256_hex(raw_token.as_bytes());
219
220 let session = SessionRepository::create(
221 &self.storage,
222 CreateSession {
223 user_id: target_id,
224 token_hash,
225 device_info: serde_json::json!({ "impersonated_by": admin_id }),
226 ip_address: format!("impersonation:{admin_id}@{admin_ip}"),
227 org_id: None,
228 expires_at: Utc::now() + chrono::Duration::seconds(self.session_ttl_secs),
229 },
230 )
231 .await?;
232
233 AuditLogRepository::append(
234 &self.storage,
235 authx_core::models::CreateAuditLog {
236 user_id: Some(admin_id),
237 org_id: None,
238 action: "admin.impersonate".into(),
239 resource_type: "session".into(),
240 resource_id: Some(session.id.to_string()),
241 ip_address: Some(admin_ip.to_owned()),
242 metadata: Some(serde_json::json!({ "target_user_id": target_id })),
243 },
244 )
245 .await?;
246
247 tracing::info!(admin = %admin_id, target = %target_id, session_id = %session.id, "impersonation session created");
248 Ok((session, raw_token))
249 }
250
251 pub async fn list_sessions(&self, user_id: Uuid) -> Result<Vec<Session>> {
252 SessionRepository::find_by_user(&self.storage, user_id).await
253 }
254
255 pub async fn revoke_all_sessions(&self, admin_id: Uuid, user_id: Uuid) -> Result<()> {
256 SessionRepository::invalidate_all_for_user(&self.storage, user_id).await?;
257 self.events.emit(AuthEvent::SignOut {
258 user_id,
259 session_id: Uuid::nil(),
260 });
261 tracing::info!(admin = %admin_id, target = %user_id, "all sessions revoked by admin");
262 Ok(())
263 }
264}