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}