1use std::collections::HashSet;
2
3use axum::extract::{Extension, Path, Query};
4use axum::http::StatusCode;
5use axum::response::Json;
6use serde::{Deserialize, Serialize};
7
8use allowthem_core::AllowThem;
9use allowthem_core::error::AuthError;
10use allowthem_core::types::{RoleId, UserId};
11use allowthem_core::users::UserCursor;
12
13use super::{AdminKey, ListResponse, ManageError, ManageState};
14
15#[derive(Serialize)]
16pub struct UserResponse {
17 pub id: String,
18 pub email: String,
19 pub username: Option<String>,
20 pub email_verified: bool,
21 pub is_active: bool,
22 pub created_at: String,
23 pub updated_at: String,
24}
25
26impl UserResponse {
27 fn from_user(u: &allowthem_core::types::User) -> Self {
28 Self {
29 id: u.id.to_string(),
30 email: u.email.as_str().to_owned(),
31 username: u.username.as_ref().map(|n| n.as_str().to_owned()),
32 email_verified: u.email_verified,
33 is_active: u.is_active,
34 created_at: u.created_at.to_rfc3339(),
35 updated_at: u.updated_at.to_rfc3339(),
36 }
37 }
38}
39
40#[derive(Deserialize)]
41pub struct ListUsersQuery {
42 limit: Option<u32>,
43 cursor: Option<String>,
44}
45
46#[derive(Deserialize)]
47pub struct PatchUserRequest {
48 is_active: Option<bool>,
49 role_ids: Option<Vec<String>>,
50}
51
52fn map_auth_error(e: AuthError) -> ManageError {
53 match e {
54 AuthError::NotFound => ManageError::NotFound,
55 AuthError::Conflict(_) => ManageError::Conflict,
56 _ => ManageError::Internal(e.to_string()),
57 }
58}
59
60fn parse_user_id(s: &str) -> Result<UserId, ManageError> {
61 s.parse().map_err(|_| ManageError::NotFound)
62}
63
64async fn list(
65 Extension(ath): Extension<AllowThem>,
66 _admin: AdminKey,
67 Query(q): Query<ListUsersQuery>,
68) -> Result<Json<ListResponse<UserResponse>>, ManageError> {
69 let limit = q.limit.unwrap_or(50).min(200);
70 let cursor: Option<UserCursor> = q
71 .cursor
72 .as_deref()
73 .map(|s| {
74 UserCursor::decode(s)
75 .ok_or_else(|| ManageError::InvalidRequest("invalid cursor".into()))
76 })
77 .transpose()?;
78
79 let mut entries = ath
80 .db()
81 .list_users_paginated(limit + 1, cursor.as_ref())
82 .await
83 .map_err(map_auth_error)?;
84
85 let has_more = entries.len() > limit as usize;
86 if has_more {
87 entries.pop();
88 }
89
90 let next_cursor = if has_more {
91 entries.last().map(|e| UserCursor::from_entry(e).encode())
92 } else {
93 None
94 };
95
96 let items = entries
97 .iter()
98 .map(|e| UserResponse {
99 id: e.id.to_string(),
100 email: e.email.as_str().to_owned(),
101 username: e.username.as_ref().map(|n| n.as_str().to_owned()),
102 email_verified: false,
103 is_active: e.is_active,
104 created_at: e.created_at.to_rfc3339(),
105 updated_at: e.created_at.to_rfc3339(),
106 })
107 .collect();
108
109 Ok(Json(ListResponse { items, next_cursor }))
110}
111
112async fn get_one(
113 Extension(ath): Extension<AllowThem>,
114 _admin: AdminKey,
115 Path(id): Path<String>,
116) -> Result<Json<UserResponse>, ManageError> {
117 let uid = parse_user_id(&id)?;
118 let user = ath.db().get_user(uid).await.map_err(map_auth_error)?;
119 Ok(Json(UserResponse::from_user(&user)))
120}
121
122async fn update(
123 Extension(ath): Extension<AllowThem>,
124 _admin: AdminKey,
125 Path(id): Path<String>,
126 Json(req): Json<PatchUserRequest>,
127) -> Result<Json<UserResponse>, ManageError> {
128 let uid = parse_user_id(&id)?;
129
130 if let Some(is_active) = req.is_active {
131 ath.db()
132 .update_user_active(uid, is_active)
133 .await
134 .map_err(map_auth_error)?;
135 }
136
137 if let Some(role_ids) = req.role_ids {
138 let requested: Vec<RoleId> = role_ids
139 .iter()
140 .map(|s| {
141 s.parse()
142 .map_err(|_| ManageError::InvalidRequest(format!("invalid role_id: {s}")))
143 })
144 .collect::<Result<_, _>>()?;
145
146 let current = ath
147 .db()
148 .get_user_roles(&uid)
149 .await
150 .map_err(map_auth_error)?;
151 let current_ids: HashSet<RoleId> = current.into_iter().map(|r| r.id).collect();
152 let requested_ids: HashSet<RoleId> = requested.into_iter().collect();
153
154 for &id in requested_ids.difference(¤t_ids) {
155 ath.db()
156 .assign_role(&uid, &id)
157 .await
158 .map_err(map_auth_error)?;
159 }
160 for &id in current_ids.difference(&requested_ids) {
161 ath.db()
162 .unassign_role(&uid, &id)
163 .await
164 .map_err(map_auth_error)?;
165 }
166 }
167
168 let user = ath.db().get_user(uid).await.map_err(map_auth_error)?;
169 Ok(Json(UserResponse::from_user(&user)))
170}
171
172async fn delete_user(
173 Extension(ath): Extension<AllowThem>,
174 _admin: AdminKey,
175 Path(id): Path<String>,
176) -> Result<StatusCode, ManageError> {
177 let uid = parse_user_id(&id)?;
178 ath.db().delete_user(uid).await.map_err(map_auth_error)?;
179 Ok(StatusCode::NO_CONTENT)
180}
181
182pub fn user_routes() -> axum::Router<ManageState> {
183 use axum::routing::get;
184 axum::Router::new()
185 .route("/", get(list))
186 .route("/{id}", get(get_one).patch(update).delete(delete_user))
187 .route("/{id}/sessions", get(super::sessions::list_for_user))
188}
189
190#[cfg(test)]
191mod tests {
192 use std::path::PathBuf;
193 use std::sync::Arc;
194
195 use axum::Router;
196 use axum::body::{Body, to_bytes};
197 use axum::http::{Request, StatusCode};
198 use chrono::Utc;
199 use tower::ServiceExt;
200 use uuid::Uuid;
201
202 use allowthem_core::types::Email;
203 use allowthem_core::{AllowThem, AllowThemBuilder};
204
205 use super::*;
206 use crate::api_keys::{ApiKey, ApiKeyId, ApiKeyScope};
207 use crate::cache::HandleCache;
208 use crate::control_db::ControlDb;
209 use crate::manage::ManageState;
210 use crate::tenants::{TenantBuilderConfig, TenantId};
211
212 async fn make_handle() -> AllowThem {
213 AllowThemBuilder::new("sqlite::memory:")
214 .cookie_secure(false)
215 .build()
216 .await
217 .unwrap()
218 }
219
220 async fn make_state() -> ManageState {
221 use std::str::FromStr;
222 let opts = sqlx::sqlite::SqliteConnectOptions::from_str("sqlite::memory:")
223 .unwrap()
224 .pragma("foreign_keys", "ON");
225 let pool = sqlx::SqlitePool::connect_with(opts).await.unwrap();
226 let ctrl = Arc::new(ControlDb::new(pool).await.unwrap());
227 ManageState::new(
228 ctrl,
229 HandleCache::new(1),
230 PathBuf::from("/tmp"),
231 Arc::new(TenantBuilderConfig {
232 mfa_key: [0u8; 32],
233 signing_key: [0u8; 32],
234 csrf_key: [0u8; 32],
235 base_domain: "test.example.com".into(),
236 is_production: false,
237 email_sender: None,
238 event_sink: None,
239 event_sink_factory: None,
240 mau_sink: None,
241 email_sender_factory: None,
242 }),
243 1000,
244 Arc::new(crate::dns::MockDnsResolver::new()),
245 )
246 }
247
248 fn make_admin_key() -> ApiKey {
249 ApiKey {
250 id: ApiKeyId::from_uuid(Uuid::nil()),
251 tenant_id: TenantId::from(Uuid::nil()),
252 name: "test".into(),
253 scope: vec![ApiKeyScope::Admin],
254 created_at: Utc::now(),
255 expires_at: None,
256 last_used_at: None,
257 }
258 }
259
260 async fn test_router(handle: AllowThem) -> Router {
261 let state = make_state().await;
262 Router::<ManageState>::new()
263 .merge(user_routes())
264 .layer(axum::Extension(handle))
265 .layer(axum::Extension(make_admin_key()))
266 .with_state(state)
267 }
268
269 async fn create_user(handle: &AllowThem, tag: u32) -> allowthem_core::types::User {
270 let email = Email::new(format!("user{tag}@test.com")).unwrap();
271 handle
272 .db()
273 .create_user(email, "pw123456", None, None)
274 .await
275 .unwrap()
276 }
277
278 #[tokio::test]
279 async fn list_users_returns_empty() {
280 let h = make_handle().await;
281 let app = test_router(h).await;
282 let req = Request::get("/").body(Body::empty()).unwrap();
283 let resp = app.oneshot(req).await.unwrap();
284 assert_eq!(resp.status(), StatusCode::OK);
285 let body = to_bytes(resp.into_body(), 4096).await.unwrap();
286 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
287 assert_eq!(json["items"].as_array().unwrap().len(), 0);
288 }
289
290 #[tokio::test]
291 async fn list_users_cursor_returns_next() {
292 let h = make_handle().await;
293 for i in 0..5u32 {
294 create_user(&h, i).await;
295 }
296 let app = test_router(h).await;
297 let req = Request::get("/?limit=3").body(Body::empty()).unwrap();
298 let resp = app.oneshot(req).await.unwrap();
299 assert_eq!(resp.status(), StatusCode::OK);
300 let body = to_bytes(resp.into_body(), 4096).await.unwrap();
301 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
302 assert_eq!(json["items"].as_array().unwrap().len(), 3);
303 assert!(!json["next_cursor"].is_null());
304 }
305
306 #[tokio::test]
307 async fn get_user_returns_data() {
308 let h = make_handle().await;
309 let user = create_user(&h, 1).await;
310 let app = test_router(h).await;
311 let req = Request::get(format!("/{}", user.id))
312 .body(Body::empty())
313 .unwrap();
314 let resp = app.oneshot(req).await.unwrap();
315 assert_eq!(resp.status(), StatusCode::OK);
316 let body = to_bytes(resp.into_body(), 4096).await.unwrap();
317 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
318 assert_eq!(json["id"], user.id.to_string());
319 }
320
321 #[tokio::test]
322 async fn get_user_not_found_returns_404() {
323 let h = make_handle().await;
324 let app = test_router(h).await;
325 let req = Request::get(format!("/{}", Uuid::nil()))
326 .body(Body::empty())
327 .unwrap();
328 let resp = app.oneshot(req).await.unwrap();
329 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
330 }
331
332 #[tokio::test]
333 async fn patch_user_toggles_active() {
334 let h = make_handle().await;
335 let user = create_user(&h, 2).await;
336 let app = test_router(h).await;
337 let body = serde_json::json!({ "is_active": false }).to_string();
338 let req = Request::patch(format!("/{}", user.id))
339 .header("content-type", "application/json")
340 .body(Body::from(body))
341 .unwrap();
342 let resp = app.oneshot(req).await.unwrap();
343 assert_eq!(resp.status(), StatusCode::OK);
344 let body = to_bytes(resp.into_body(), 4096).await.unwrap();
345 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
346 assert_eq!(json["is_active"], false);
347 }
348
349 #[tokio::test]
350 async fn patch_user_diff_roles() {
351 let h = make_handle().await;
352 let user = create_user(&h, 3).await;
353 let role = h
354 .db()
355 .create_role(&allowthem_core::types::RoleName::new("editor"), None)
356 .await
357 .unwrap();
358 let app = test_router(h).await;
359 let body = serde_json::json!({ "role_ids": [role.id.to_string()] }).to_string();
360 let req = Request::patch(format!("/{}", user.id))
361 .header("content-type", "application/json")
362 .body(Body::from(body))
363 .unwrap();
364 let resp = app.oneshot(req).await.unwrap();
365 assert_eq!(resp.status(), StatusCode::OK);
366 }
367
368 #[tokio::test]
369 async fn delete_user_returns_204() {
370 let h = make_handle().await;
371 let user = create_user(&h, 4).await;
372 let app = test_router(h).await;
373 let req = Request::delete(format!("/{}", user.id))
374 .body(Body::empty())
375 .unwrap();
376 let resp = app.oneshot(req).await.unwrap();
377 assert_eq!(resp.status(), StatusCode::NO_CONTENT);
378 }
379
380 #[tokio::test]
381 async fn delete_user_not_found_returns_404() {
382 let h = make_handle().await;
383 let app = test_router(h).await;
384 let req = Request::delete(format!("/{}", Uuid::nil()))
385 .body(Body::empty())
386 .unwrap();
387 let resp = app.oneshot(req).await.unwrap();
388 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
389 }
390}