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