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}