Skip to main content

dinio_sdk/api/
client_identities.rs

1//! `/api/v1/client-identities` (`../core/src/api/client_identities.ts`). An identity is
2//! owned by the user who created it (`../core/src/api/ownership.ts`).
3
4use reqwest::Method;
5use serde::Serialize;
6use uuid::Uuid;
7
8use crate::client::Client;
9use crate::error::{Error, Result};
10use crate::models::{ClientIdentity, ClientIdentityPatch, NewClientIdentity};
11
12/// Optional filters for [`ClientIdentities::list`].
13#[derive(Debug, Clone, Default, Serialize)]
14pub struct ClientIdentityFilter {
15    /// Only the identity with this uuid.
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub uuid: Option<Uuid>,
18    /// Only the identity with exactly this `identify` value.
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub identify: Option<String>,
21}
22
23/// Client identity routes, from [`Client::client_identities`].
24///
25/// With a personal key you see and change only your own identities (all of them as an
26/// Admin). A zone key has no access: every route answers
27/// [`ErrorCode::Forbidden`](crate::ErrorCode::Forbidden).
28pub struct ClientIdentities<'a> {
29    client: &'a Client,
30}
31
32impl<'a> ClientIdentities<'a> {
33    pub(crate) fn new(client: &'a Client) -> Self {
34        Self { client }
35    }
36
37    /// The caller's identities, all at once (Core does not paginate them).
38    ///
39    /// `GET /api/v1/client-identities`
40    pub async fn list(&self, filter: &ClientIdentityFilter) -> Result<Vec<ClientIdentity>> {
41        let request = self
42            .client
43            .request(Method::GET, &["client-identities"])?
44            .query(filter);
45        Ok(self.client.send(request).await?.0)
46    }
47
48    /// One identity, by uuid or by its `identify` value (e.g. `203.0.113.10`).
49    ///
50    /// `GET /api/v1/client-identities/:ref`
51    ///
52    /// # Errors
53    ///
54    /// * [`ErrorCode::NotFound`](crate::ErrorCode::NotFound) if there is no such identity.
55    /// * [`ErrorCode::Forbidden`](crate::ErrorCode::Forbidden) if it belongs to another
56    ///   user.
57    ///
58    /// # Examples
59    ///
60    /// ```no_run
61    /// # async fn example(client: &dinio_sdk::Client) -> dinio_sdk::Result<()> {
62    /// let identity = client.client_identities().get("203.0.113.10").await?;
63    /// println!("{} is {}", identity.identify, identity.uuid);
64    /// # Ok(())
65    /// # }
66    /// ```
67    pub async fn get(&self, identity: &str) -> Result<ClientIdentity> {
68        let request = self
69            .client
70            .request(Method::GET, &["client-identities", identity])?;
71        Ok(self.client.send(request).await?.0)
72    }
73
74    /// Creates an identity owned by the caller; Relay starts recognising the client. For
75    /// `Hostname`/`Path` the returned identity carries the `identify` Core generated.
76    ///
77    /// `POST /api/v1/client-identities`
78    ///
79    /// # Errors
80    ///
81    /// * [`Error::InvalidInput`] if `identify` is given for `Hostname`/`Path`, or missing
82    ///   for `Ip`/`None` (checked before sending).
83    /// * [`ErrorCode::Conflict`](crate::ErrorCode::Conflict) if the IP address is already
84    ///   in use, by anyone.
85    /// * [`ErrorCode::Validation`](crate::ErrorCode::Validation) if `identify` is not an IP
86    ///   address.
87    ///
88    /// # Examples
89    ///
90    /// ```no_run
91    /// use dinio_sdk::models::{ClientIdentityType, NewClientIdentity};
92    ///
93    /// # async fn example(client: &dinio_sdk::Client) -> dinio_sdk::Result<()> {
94    /// let new = NewClientIdentity {
95    ///     identify: None,
96    ///     id_type: ClientIdentityType::Path,
97    ///     name: Some("phone".into()),
98    /// };
99    /// let identity = client.client_identities().create(&new).await?;
100    /// println!("DoH path part: {}", identity.identify);
101    /// # Ok(())
102    /// # }
103    /// ```
104    pub async fn create(&self, identity: &NewClientIdentity) -> Result<ClientIdentity> {
105        match (identity.id_type.generates_identify(), &identity.identify) {
106            (true, Some(_)) => {
107                return Err(Error::InvalidInput(format!(
108                    "Core generates identify for a {} identity; leave it out",
109                    identity.id_type
110                )));
111            }
112            (false, None) => {
113                return Err(Error::InvalidInput(format!(
114                    "a {} identity needs identify (the client's IP address)",
115                    identity.id_type
116                )));
117            }
118            _ => {}
119        }
120        let request = self
121            .client
122            .request(Method::POST, &["client-identities"])?
123            .json(identity);
124        Ok(self.client.send(request).await?.0)
125    }
126
127    /// Changes an identity; fields left `None` stay as they are.
128    ///
129    /// `PATCH /api/v1/client-identities/:uuid`
130    ///
131    /// # Errors
132    ///
133    /// * [`ErrorCode::NotFound`](crate::ErrorCode::NotFound) if there is no such identity.
134    /// * [`ErrorCode::Forbidden`](crate::ErrorCode::Forbidden) if it belongs to another
135    ///   user.
136    /// * [`ErrorCode::Suspended`](crate::ErrorCode::Suspended) if the identity or its
137    ///   owner is suspended.
138    /// * [`ErrorCode::Conflict`](crate::ErrorCode::Conflict) if the new `identify` is in
139    ///   use.
140    /// * [`ErrorCode::Validation`](crate::ErrorCode::Validation) if the result would not
141    ///   pair `identify` and `id_type` correctly (see [`ClientIdentityPatch`]).
142    pub async fn update(&self, uuid: Uuid, patch: &ClientIdentityPatch) -> Result<ClientIdentity> {
143        let request = self
144            .client
145            .request(Method::PATCH, &["client-identities", &uuid.to_string()])?
146            .json(patch);
147        Ok(self.client.send(request).await?.0)
148    }
149
150    /// Replaces the generated `identify` of a `Hostname`/`Path` identity with a new one.
151    /// The old value stops working on Relay, so every device configured with it needs the
152    /// new one.
153    ///
154    /// `POST /api/v1/client-identities/:uuid/identify`
155    ///
156    /// # Errors
157    ///
158    /// * [`ErrorCode::NotFound`](crate::ErrorCode::NotFound) if there is no such identity.
159    /// * [`ErrorCode::Forbidden`](crate::ErrorCode::Forbidden) if it belongs to another
160    ///   user.
161    /// * [`ErrorCode::Suspended`](crate::ErrorCode::Suspended) if the identity or its
162    ///   owner is suspended.
163    /// * [`ErrorCode::Validation`](crate::ErrorCode::Validation) for an `Ip`/`None`
164    ///   identity, whose `identify` is an address, not generated.
165    pub async fn regenerate_identify(&self, uuid: Uuid) -> Result<ClientIdentity> {
166        let request = self.client.request(
167            Method::POST,
168            &["client-identities", &uuid.to_string(), "identify"],
169        )?;
170        Ok(self.client.send(request).await?.0)
171    }
172
173    /// Deletes an identity.
174    ///
175    /// `DELETE /api/v1/client-identities/:uuid`
176    ///
177    /// # Errors
178    ///
179    /// * [`ErrorCode::NotFound`](crate::ErrorCode::NotFound) if there is no such identity.
180    /// * [`ErrorCode::Forbidden`](crate::ErrorCode::Forbidden) if it belongs to another
181    ///   user.
182    /// * [`ErrorCode::Conflict`](crate::ErrorCode::Conflict) while sinkhole lists are
183    ///   linked to it; remove the links first
184    ///   ([`SinkholeLinks::delete`](crate::api::SinkholeLinks::delete)).
185    pub async fn delete(&self, uuid: Uuid) -> Result<()> {
186        let request = self
187            .client
188            .request(Method::DELETE, &["client-identities", &uuid.to_string()])?;
189        self.client.send_empty(request).await
190    }
191}