files_sdk/users/users.rs
1//! User management operations
2//!
3//! Provides comprehensive user account management for Files.com sites, including
4//! creation, modification, and administrative operations.
5//!
6//! # Features
7//!
8//! - Create and manage user accounts
9//! - Configure permissions and access controls
10//! - Set user quotas and restrictions
11//! - Administrative operations (unlock, reset 2FA)
12//! - Group membership management
13//! - Protocol-specific permissions (FTP, SFTP, WebDAV, REST API)
14//!
15//! # Example
16//!
17//! ```no_run
18//! use files_sdk::{FilesClient, UserHandler};
19//!
20//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
21//! let client = FilesClient::builder()
22//! .api_key("your-api-key")
23//! .build()?;
24//!
25//! let handler = UserHandler::new(client);
26//!
27//! // Create a new user with SFTP access
28//! let user = handler.create(
29//! "newuser",
30//! Some("user@company.com"),
31//! Some("secure-password"),
32//! Some("New User")
33//! ).await?;
34//!
35//! println!("Created user ID: {}", user.id.unwrap());
36//!
37//! // List all users
38//! let (users, _) = handler.list(None, Some(100)).await?;
39//! for user in users {
40//! println!("User: {} ({})",
41//! user.username.unwrap_or_default(),
42//! user.email.unwrap_or_default());
43//! }
44//! # Ok(())
45//! # }
46//! ```
47
48use crate::{FilesClient, PaginationInfo, Result};
49use futures::stream::Stream;
50use serde::{Deserialize, Serialize};
51use serde_json::json;
52
53/// User entity from Files.com API
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct UserEntity {
56 /// User ID
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub id: Option<i64>,
59
60 /// Username
61 #[serde(skip_serializing_if = "Option::is_none")]
62 pub username: Option<String>,
63
64 /// User's email address
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub email: Option<String>,
67
68 /// User's full name
69 #[serde(skip_serializing_if = "Option::is_none")]
70 pub name: Option<String>,
71
72 /// Company name
73 #[serde(skip_serializing_if = "Option::is_none")]
74 pub company: Option<String>,
75
76 /// Notes about the user
77 #[serde(skip_serializing_if = "Option::is_none")]
78 pub notes: Option<String>,
79
80 /// User home directory
81 #[serde(skip_serializing_if = "Option::is_none")]
82 pub user_home: Option<String>,
83
84 /// User root directory
85 #[serde(skip_serializing_if = "Option::is_none")]
86 pub user_root: Option<String>,
87
88 /// Is site admin
89 #[serde(skip_serializing_if = "Option::is_none")]
90 pub site_admin: Option<bool>,
91
92 /// Is read-only site admin
93 #[serde(skip_serializing_if = "Option::is_none")]
94 pub readonly_site_admin: Option<bool>,
95
96 /// User is disabled
97 #[serde(skip_serializing_if = "Option::is_none")]
98 pub disabled: Option<bool>,
99
100 /// User is disabled, expired, or inactive
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub disabled_expired_or_inactive: Option<bool>,
103
104 /// SSL/TLS is required for this user
105 #[serde(skip_serializing_if = "Option::is_none")]
106 pub ssl_required: Option<String>,
107
108 /// Time zone
109 #[serde(skip_serializing_if = "Option::is_none")]
110 pub time_zone: Option<String>,
111
112 /// Language preference
113 #[serde(skip_serializing_if = "Option::is_none")]
114 pub language: Option<String>,
115
116 /// Allowed IP addresses
117 #[serde(skip_serializing_if = "Option::is_none")]
118 pub allowed_ips: Option<String>,
119
120 /// Bypass site allowed IPs
121 #[serde(skip_serializing_if = "Option::is_none")]
122 pub bypass_site_allowed_ips: Option<bool>,
123
124 /// Group IDs this user belongs to (can be string or array)
125 #[serde(skip_serializing_if = "Option::is_none")]
126 pub group_ids: Option<String>,
127
128 /// Admin group IDs
129 #[serde(skip_serializing_if = "Option::is_none")]
130 pub admin_group_ids: Option<Vec<i64>>,
131
132 /// FTP permission
133 #[serde(skip_serializing_if = "Option::is_none")]
134 pub ftp_permission: Option<bool>,
135
136 /// SFTP permission
137 #[serde(skip_serializing_if = "Option::is_none")]
138 pub sftp_permission: Option<bool>,
139
140 /// WebDAV permission
141 #[serde(skip_serializing_if = "Option::is_none")]
142 pub dav_permission: Option<bool>,
143
144 /// REST API permission
145 #[serde(skip_serializing_if = "Option::is_none")]
146 pub restapi_permission: Option<bool>,
147
148 /// Require 2FA
149 #[serde(skip_serializing_if = "Option::is_none")]
150 pub require_2fa: Option<String>,
151
152 /// Active 2FA method
153 #[serde(skip_serializing_if = "Option::is_none")]
154 pub active_2fa: Option<bool>,
155
156 /// Created at timestamp
157 #[serde(skip_serializing_if = "Option::is_none")]
158 pub created_at: Option<String>,
159
160 /// Last login at timestamp
161 #[serde(skip_serializing_if = "Option::is_none")]
162 pub last_login_at: Option<String>,
163
164 /// Password set at timestamp
165 #[serde(skip_serializing_if = "Option::is_none")]
166 pub password_set_at: Option<String>,
167
168 /// Password validity in days
169 #[serde(skip_serializing_if = "Option::is_none")]
170 pub password_validity_days: Option<i64>,
171
172 /// API keys count
173 #[serde(skip_serializing_if = "Option::is_none")]
174 pub api_keys_count: Option<i64>,
175
176 /// Public keys count
177 #[serde(skip_serializing_if = "Option::is_none")]
178 pub public_keys_count: Option<i64>,
179}
180
181/// Handler for user operations
182#[derive(Debug, Clone)]
183pub struct UserHandler {
184 client: FilesClient,
185}
186
187impl UserHandler {
188 /// Creates a new UserHandler
189 pub fn new(client: FilesClient) -> Self {
190 Self { client }
191 }
192
193 /// List all users on the site
194 ///
195 /// Returns a paginated list of user accounts with their details and permissions.
196 ///
197 /// # Arguments
198 ///
199 /// * `cursor` - Pagination cursor from previous response
200 /// * `per_page` - Number of results per page (default 100, max 10,000)
201 ///
202 /// # Returns
203 ///
204 /// A tuple containing:
205 /// - Vector of `UserEntity` objects
206 /// - `PaginationInfo` with cursors for next/previous pages
207 ///
208 /// # Example
209 ///
210 /// ```no_run
211 /// use files_sdk::{FilesClient, UserHandler};
212 ///
213 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
214 /// let client = FilesClient::builder().api_key("key").build()?;
215 /// let handler = UserHandler::new(client);
216 ///
217 /// // List first page of users
218 /// let (users, pagination) = handler.list(None, Some(50)).await?;
219 ///
220 /// for user in users {
221 /// println!("{}: {} - Admin: {}",
222 /// user.username.unwrap_or_default(),
223 /// user.email.unwrap_or_default(),
224 /// user.site_admin.unwrap_or(false));
225 /// }
226 ///
227 /// // Get next page if available
228 /// if let Some(next) = pagination.cursor_next {
229 /// let (more_users, _) = handler.list(Some(next), Some(50)).await?;
230 /// }
231 /// # Ok(())
232 /// # }
233 /// ```
234 pub async fn list(
235 &self,
236 cursor: Option<String>,
237 per_page: Option<i32>,
238 ) -> Result<(Vec<UserEntity>, PaginationInfo)> {
239 let mut path = "/users?".to_string();
240
241 if let Some(c) = cursor {
242 path.push_str(&format!("cursor={}&", c));
243 }
244 if let Some(pp) = per_page {
245 path.push_str(&format!("per_page={}&", pp));
246 }
247
248 let response = self.client.get_raw(&path).await?;
249 let users: Vec<UserEntity> = serde_json::from_value(response)?;
250
251 // TODO: Extract pagination info from response headers
252 let pagination = PaginationInfo {
253 cursor_next: None,
254 cursor_prev: None,
255 };
256
257 Ok((users, pagination))
258 }
259
260 /// Stream all users with automatic pagination
261 ///
262 /// Returns a stream that automatically handles pagination, yielding
263 /// individual users as they are fetched. This is more memory-efficient
264 /// for large user lists.
265 ///
266 /// # Arguments
267 ///
268 /// * `per_page` - Number of users per page (optional, default 1000)
269 ///
270 /// # Examples
271 ///
272 /// ```rust,no_run
273 /// # use files_sdk::{FilesClient, UserHandler};
274 /// # use futures::stream::StreamExt;
275 /// # #[tokio::main]
276 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
277 /// # let client = FilesClient::builder().api_key("key").build()?;
278 /// let handler = UserHandler::new(client);
279 /// let stream = handler.list_stream(Some(100));
280 ///
281 /// tokio::pin!(stream);
282 ///
283 /// while let Some(user) = stream.next().await {
284 /// let user = user?;
285 /// println!("{}", user.username.unwrap_or_default());
286 /// }
287 /// # Ok(())
288 /// # }
289 /// ```
290 pub fn list_stream(
291 &self,
292 per_page: Option<i32>,
293 ) -> impl Stream<Item = Result<UserEntity>> + '_ {
294 let per_page = per_page.unwrap_or(1000);
295
296 async_stream::try_stream! {
297 let mut cursor: Option<String> = None;
298
299 loop {
300 let (users, pagination) = self
301 .list(cursor.clone(), Some(per_page))
302 .await?;
303
304 for user in users {
305 yield user;
306 }
307
308 match pagination.cursor_next {
309 Some(next) => cursor = Some(next),
310 None => break,
311 }
312 }
313 }
314 }
315
316 /// Get details of a specific user by ID
317 ///
318 /// # Arguments
319 ///
320 /// * `id` - The unique user ID
321 ///
322 /// # Returns
323 ///
324 /// A `UserEntity` with complete user information
325 ///
326 /// # Example
327 ///
328 /// ```no_run
329 /// use files_sdk::{FilesClient, UserHandler};
330 ///
331 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
332 /// let client = FilesClient::builder().api_key("key").build()?;
333 /// let handler = UserHandler::new(client);
334 ///
335 /// let user = handler.get(12345).await?;
336 /// println!("User: {}", user.username.unwrap_or_default());
337 /// println!("Last login: {}", user.last_login_at.unwrap_or_default());
338 /// # Ok(())
339 /// # }
340 /// ```
341 pub async fn get(&self, id: i64) -> Result<UserEntity> {
342 let path = format!("/users/{}", id);
343 let response = self.client.get_raw(&path).await?;
344 Ok(serde_json::from_value(response)?)
345 }
346
347 /// Create a new user account
348 ///
349 /// Creates a new user with the specified credentials. The user will receive
350 /// a welcome email if email is provided.
351 ///
352 /// # Arguments
353 ///
354 /// * `username` - Unique username for login (required)
355 /// * `email` - User's email address
356 /// * `password` - Initial password (if not provided, user must set on first login)
357 /// * `name` - User's full name
358 ///
359 /// # Returns
360 ///
361 /// The newly created `UserEntity`
362 ///
363 /// # Example
364 ///
365 /// ```no_run
366 /// use files_sdk::{FilesClient, UserHandler};
367 ///
368 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
369 /// let client = FilesClient::builder().api_key("key").build()?;
370 /// let handler = UserHandler::new(client);
371 ///
372 /// // Create user with all details
373 /// let user = handler.create(
374 /// "jdoe",
375 /// Some("jdoe@company.com"),
376 /// Some("SecureP@ssw0rd"),
377 /// Some("John Doe")
378 /// ).await?;
379 ///
380 /// println!("Created user: {} (ID: {})",
381 /// user.username.unwrap(),
382 /// user.id.unwrap());
383 /// # Ok(())
384 /// # }
385 /// ```
386 pub async fn create(
387 &self,
388 username: &str,
389 email: Option<&str>,
390 password: Option<&str>,
391 name: Option<&str>,
392 ) -> Result<UserEntity> {
393 let mut body = json!({
394 "username": username,
395 });
396
397 if let Some(e) = email {
398 body["email"] = json!(e);
399 }
400 if let Some(p) = password {
401 body["password"] = json!(p);
402 }
403 if let Some(n) = name {
404 body["name"] = json!(n);
405 }
406
407 let response = self.client.post_raw("/users", body).await?;
408 Ok(serde_json::from_value(response)?)
409 }
410
411 /// Update user information
412 ///
413 /// Updates user profile information. Only provided fields are updated;
414 /// omitted fields remain unchanged.
415 ///
416 /// # Arguments
417 ///
418 /// * `id` - User ID to update
419 /// * `email` - New email address
420 /// * `name` - New full name
421 /// * `company` - New company name
422 /// * `notes` - Administrative notes about the user
423 ///
424 /// # Returns
425 ///
426 /// The updated `UserEntity`
427 ///
428 /// # Example
429 ///
430 /// ```no_run
431 /// use files_sdk::{FilesClient, UserHandler};
432 ///
433 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
434 /// let client = FilesClient::builder().api_key("key").build()?;
435 /// let handler = UserHandler::new(client);
436 ///
437 /// // Update user's company and notes
438 /// let user = handler.update(
439 /// 12345,
440 /// None,
441 /// None,
442 /// Some("New Company Inc."),
443 /// Some("Transferred from old company")
444 /// ).await?;
445 /// # Ok(())
446 /// # }
447 /// ```
448 pub async fn update(
449 &self,
450 id: i64,
451 email: Option<&str>,
452 name: Option<&str>,
453 company: Option<&str>,
454 notes: Option<&str>,
455 ) -> Result<UserEntity> {
456 let mut body = json!({});
457
458 if let Some(e) = email {
459 body["email"] = json!(e);
460 }
461 if let Some(n) = name {
462 body["name"] = json!(n);
463 }
464 if let Some(c) = company {
465 body["company"] = json!(c);
466 }
467 if let Some(nt) = notes {
468 body["notes"] = json!(nt);
469 }
470
471 let path = format!("/users/{}", id);
472 let response = self.client.patch_raw(&path, body).await?;
473 Ok(serde_json::from_value(response)?)
474 }
475
476 /// Delete a user account permanently
477 ///
478 /// Removes the user account and all associated data. This operation cannot
479 /// be undone.
480 ///
481 /// # Arguments
482 ///
483 /// * `id` - User ID to delete
484 ///
485 /// # Example
486 ///
487 /// ```no_run
488 /// use files_sdk::{FilesClient, UserHandler};
489 ///
490 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
491 /// let client = FilesClient::builder().api_key("key").build()?;
492 /// let handler = UserHandler::new(client);
493 ///
494 /// handler.delete(12345).await?;
495 /// println!("User deleted successfully");
496 /// # Ok(())
497 /// # }
498 /// ```
499 pub async fn delete(&self, id: i64) -> Result<()> {
500 let path = format!("/users/{}", id);
501 self.client.delete_raw(&path).await?;
502 Ok(())
503 }
504
505 /// Unlock a locked user account
506 ///
507 /// Removes the lock from a user account that has been locked due to too many
508 /// failed login attempts or administrative action.
509 ///
510 /// # Arguments
511 ///
512 /// * `id` - User ID to unlock
513 ///
514 /// # Example
515 ///
516 /// ```no_run
517 /// use files_sdk::{FilesClient, UserHandler};
518 ///
519 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
520 /// let client = FilesClient::builder().api_key("key").build()?;
521 /// let handler = UserHandler::new(client);
522 ///
523 /// // Unlock user after failed login attempts
524 /// handler.unlock(12345).await?;
525 /// println!("User unlocked successfully");
526 /// # Ok(())
527 /// # }
528 /// ```
529 pub async fn unlock(&self, id: i64) -> Result<()> {
530 let path = format!("/users/{}/unlock", id);
531 self.client.post_raw(&path, json!({})).await?;
532 Ok(())
533 }
534
535 /// Reset two-factor authentication for a user
536 ///
537 /// Disables 2FA for the user, requiring them to set it up again on next login.
538 /// Use this when a user has lost access to their 2FA device.
539 ///
540 /// # Arguments
541 ///
542 /// * `id` - User ID
543 ///
544 /// # Example
545 ///
546 /// ```no_run
547 /// use files_sdk::{FilesClient, UserHandler};
548 ///
549 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
550 /// let client = FilesClient::builder().api_key("key").build()?;
551 /// let handler = UserHandler::new(client);
552 ///
553 /// // Reset 2FA for user who lost their device
554 /// handler.reset_2fa(12345).await?;
555 /// println!("2FA reset - user must configure on next login");
556 /// # Ok(())
557 /// # }
558 /// ```
559 pub async fn reset_2fa(&self, id: i64) -> Result<()> {
560 let path = format!("/users/{}/2fa/reset", id);
561 self.client.post_raw(&path, json!({})).await?;
562 Ok(())
563 }
564
565 /// Resend welcome email to a user
566 ///
567 /// Sends a new welcome email to the user with login instructions and
568 /// password setup link if needed.
569 ///
570 /// # Arguments
571 ///
572 /// * `id` - User ID
573 ///
574 /// # Example
575 ///
576 /// ```no_run
577 /// use files_sdk::{FilesClient, UserHandler};
578 ///
579 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
580 /// let client = FilesClient::builder().api_key("key").build()?;
581 /// let handler = UserHandler::new(client);
582 ///
583 /// // Resend welcome email to new user
584 /// handler.resend_welcome_email(12345).await?;
585 /// println!("Welcome email sent");
586 /// # Ok(())
587 /// # }
588 /// ```
589 pub async fn resend_welcome_email(&self, id: i64) -> Result<()> {
590 let path = format!("/users/{}/resend_welcome_email", id);
591 self.client.post_raw(&path, json!({})).await?;
592 Ok(())
593 }
594}
595
596#[cfg(test)]
597mod tests {
598 use super::*;
599
600 #[test]
601 fn test_handler_creation() {
602 let client = FilesClient::builder().api_key("test-key").build().unwrap();
603 let _handler = UserHandler::new(client);
604 }
605}