Skip to main content

claude_api/admin/
users.rs

1//! Users: retrieve / list / update / delete.
2
3use serde::{Deserialize, Serialize};
4
5use crate::client::Client;
6use crate::error::Result;
7use crate::pagination::Paginated;
8
9use super::{ListParams, OrganizationRole, WriteOrganizationRole};
10
11/// An organization user.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13#[non_exhaustive]
14pub struct User {
15    /// Stable user ID.
16    pub id: String,
17    /// Wire type tag (`"user"`).
18    #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
19    pub ty: Option<String>,
20    /// Email.
21    pub email: String,
22    /// Display name.
23    pub name: String,
24    /// Role.
25    pub role: OrganizationRole,
26    /// RFC3339 timestamp when the user joined.
27    pub added_at: String,
28}
29
30/// Response shape for `DELETE /v1/organizations/users/{id}`.
31#[derive(Debug, Clone, Serialize, Deserialize)]
32#[non_exhaustive]
33pub struct UserDeleted {
34    /// ID of the deleted user.
35    pub id: String,
36    /// Wire type tag (`"user_deleted"`).
37    #[serde(rename = "type")]
38    pub ty: String,
39}
40
41/// Request body for `POST /v1/organizations/users/{id}` (update).
42#[derive(Debug, Clone, Serialize)]
43#[non_exhaustive]
44pub struct UpdateUserRequest {
45    /// New role. Cannot be `admin`.
46    pub role: WriteOrganizationRole,
47}
48
49impl UpdateUserRequest {
50    /// Build with a new role.
51    #[must_use]
52    pub fn new(role: WriteOrganizationRole) -> Self {
53        Self { role }
54    }
55}
56
57/// Filters for [`Users::list`].
58#[derive(Debug, Clone, Default)]
59#[non_exhaustive]
60pub struct ListUsersParams {
61    /// Underlying pagination params.
62    pub paging: ListParams,
63    /// Filter to a specific email.
64    pub email: Option<String>,
65}
66
67impl ListUsersParams {
68    fn to_query(&self) -> Vec<(&'static str, String)> {
69        let mut q = self.paging.to_query();
70        if let Some(e) = &self.email {
71            q.push(("email", e.clone()));
72        }
73        q
74    }
75}
76
77/// Namespace handle for user endpoints.
78pub struct Users<'a> {
79    client: &'a Client,
80}
81
82impl<'a> Users<'a> {
83    pub(crate) fn new(client: &'a Client) -> Self {
84        Self { client }
85    }
86
87    /// `GET /v1/organizations/users/{id}`.
88    pub async fn retrieve(&self, user_id: &str) -> Result<User> {
89        let path = format!("/v1/organizations/users/{user_id}");
90        self.client
91            .execute_with_retry(
92                || self.client.request_builder(reqwest::Method::GET, &path),
93                &[],
94            )
95            .await
96    }
97
98    /// `GET /v1/organizations/users`.
99    pub async fn list(&self, params: ListUsersParams) -> Result<Paginated<User>> {
100        let query = params.to_query();
101        self.client
102            .execute_with_retry(
103                || {
104                    let mut req = self
105                        .client
106                        .request_builder(reqwest::Method::GET, "/v1/organizations/users");
107                    for (k, v) in &query {
108                        req = req.query(&[(k, v)]);
109                    }
110                    req
111                },
112                &[],
113            )
114            .await
115    }
116
117    /// `POST /v1/organizations/users/{id}` (update role).
118    pub async fn update(&self, user_id: &str, request: UpdateUserRequest) -> Result<User> {
119        let path = format!("/v1/organizations/users/{user_id}");
120        let body = &request;
121        self.client
122            .execute_with_retry(
123                || {
124                    self.client
125                        .request_builder(reqwest::Method::POST, &path)
126                        .json(body)
127                },
128                &[],
129            )
130            .await
131    }
132
133    /// `DELETE /v1/organizations/users/{id}`.
134    pub async fn delete(&self, user_id: &str) -> Result<UserDeleted> {
135        let path = format!("/v1/organizations/users/{user_id}");
136        self.client
137            .execute_with_retry(
138                || self.client.request_builder(reqwest::Method::DELETE, &path),
139                &[],
140            )
141            .await
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148    use serde_json::json;
149    use wiremock::matchers::{body_partial_json, method, path};
150    use wiremock::{Mock, MockServer, ResponseTemplate};
151
152    fn client_for(mock: &MockServer) -> Client {
153        Client::builder()
154            .api_key("sk-ant-admin-test")
155            .base_url(mock.uri())
156            .build()
157            .unwrap()
158    }
159
160    fn fake_user() -> serde_json::Value {
161        json!({
162            "id": "user_01",
163            "type": "user",
164            "email": "u@example.com",
165            "name": "User",
166            "role": "developer",
167            "added_at": "2026-05-01T00:00:00Z"
168        })
169    }
170
171    #[tokio::test]
172    async fn retrieve_user_returns_typed_record() {
173        let mock = MockServer::start().await;
174        Mock::given(method("GET"))
175            .and(path("/v1/organizations/users/user_01"))
176            .respond_with(ResponseTemplate::new(200).set_body_json(fake_user()))
177            .mount(&mock)
178            .await;
179        let client = client_for(&mock);
180        let u = client.admin().users().retrieve("user_01").await.unwrap();
181        assert_eq!(u.email, "u@example.com");
182    }
183
184    #[tokio::test]
185    async fn list_users_filters_by_email() {
186        let mock = MockServer::start().await;
187        Mock::given(method("GET"))
188            .and(path("/v1/organizations/users"))
189            .and(wiremock::matchers::query_param("email", "u@example.com"))
190            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
191                "data": [fake_user()],
192                "has_more": false,
193                "first_id": "user_01",
194                "last_id": "user_01"
195            })))
196            .mount(&mock)
197            .await;
198        let client = client_for(&mock);
199        let page = client
200            .admin()
201            .users()
202            .list(ListUsersParams {
203                email: Some("u@example.com".into()),
204                ..Default::default()
205            })
206            .await
207            .unwrap();
208        assert_eq!(page.data.len(), 1);
209    }
210
211    #[tokio::test]
212    async fn update_user_role_sends_role_body() {
213        let mock = MockServer::start().await;
214        Mock::given(method("POST"))
215            .and(path("/v1/organizations/users/user_01"))
216            .and(body_partial_json(json!({"role": "user"})))
217            .respond_with(ResponseTemplate::new(200).set_body_json(fake_user()))
218            .mount(&mock)
219            .await;
220        let client = client_for(&mock);
221        client
222            .admin()
223            .users()
224            .update(
225                "user_01",
226                UpdateUserRequest::new(WriteOrganizationRole::User),
227            )
228            .await
229            .unwrap();
230    }
231
232    #[tokio::test]
233    async fn delete_user_returns_deleted_marker() {
234        let mock = MockServer::start().await;
235        Mock::given(method("DELETE"))
236            .and(path("/v1/organizations/users/user_01"))
237            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
238                "id": "user_01",
239                "type": "user_deleted"
240            })))
241            .mount(&mock)
242            .await;
243        let client = client_for(&mock);
244        let r = client.admin().users().delete("user_01").await.unwrap();
245        assert_eq!(r.ty, "user_deleted");
246    }
247}