1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
use serde::Deserialize;
use thiserror::Error;
use super::AuthStore;
use crate::{Collection, PocketBase};
/// Represents the various errors that can be obtained after a `impersonate` request.
#[derive(Error, Debug)]
pub enum ImpersonateError {
/// Communication with the `PocketBase` API was successful,
/// but returned a [400 Bad Request]("https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400") HTTP error response.
///
/// The request requires valid record authorization token to be set.
#[error("Bad Request: The request requires valid record authorization token to be set.")]
BadRequest,
/// Communication with the `PocketBase` API was successful,
/// but returned a [401 Unauthorized]("https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401") HTTP error response.
///
/// The request requires valid record authorization token.
#[error("The request requires valid record authorization token.")]
Unauthorized,
/// Communication with the `PocketBase` API was successful,
/// but returned a [403 Forbidden]("https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403") HTTP error response.
///
/// The authorized record is not allowed to perform this action.
/// Are you impersonating a user from a non-superuser account?
#[error(
"The authorized record is not allowed to perform this action. Are you impersonating a user from a non-superuser account?"
)]
Forbidden,
/// Communication with the `PocketBase` API was successful,
/// but returned a [404 Not Found]("https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404") HTTP error response.
///
/// The requested resource wasn't found.
/// The given user id is probably wrong.
#[error("The requested resource wasn't found.")]
NotFound,
/// Communication with the `PocketBase` API failed.
///
/// This could be caused by an internet outage, an error in the link given to the `PocketBase` SDK
/// and similar errors.
#[error("The communication with the PocketBase API failed: {0}")]
Unreachable(String),
/// The response from the `PocketBase` instance API was unexpected.
/// If you think its an error, please [open an issue on GitHub]("https://github.com/fromhorizons/pocketbase-rs/issues").
#[error("An unhandled status code was returned by the PocketBase API: {0}")]
UnexpectedResponse(String),
}
#[derive(Deserialize)]
struct AuthData {
record: AuthDataRecord,
token: String,
}
#[derive(Default, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
struct AuthDataRecord {
collection_id: String,
collection_name: String,
id: String,
email: String,
email_visibility: bool,
verified: bool,
created: String,
updated: String,
}
pub struct CollectionImpersonateBuilder<'a> {
client: &'a PocketBase,
collection_name: &'a str,
user_id: &'a str,
duration: Option<String>,
}
impl<'a> Collection<'a> {
/// Authenticate as a different user by generating a non-refreshable auth token.
///
/// Only superusers can perform this action. Returns a new `PocketBase` client
/// with the impersonated user's auth token.
///
/// # Example
/// ```rust,ignore
/// let impersonate_client = pb
/// .collection("users")
/// .impersonate("USER_RECORD_ID")
/// .duration(3600)
/// .call()
/// .await?;
///
/// println!("Token: {}", impersonate_client.auth_store().unwrap().token);
/// ```
#[must_use]
pub const fn impersonate(self, user_id: &'a str) -> CollectionImpersonateBuilder<'a> {
CollectionImpersonateBuilder {
client: self.client,
collection_name: self.name,
user_id,
duration: None,
}
}
}
impl CollectionImpersonateBuilder<'_> {
/// Set custom JWT duration in seconds (optional).
///
/// If not set, uses the default collection auth token duration.
pub fn duration(mut self, duration: u128) -> Self {
self.duration = Some(duration.to_string());
self
}
/// Execute the request and return a new `PocketBase` client with the impersonated user's token.
pub async fn call(self) -> Result<PocketBase, ImpersonateError> {
let url = format!(
"{}/api/collections/{}/impersonate/{}",
self.client.base_url, self.collection_name, self.user_id
);
let request = {
if let Some(duration) = self.duration {
self.client
.request_post_form(
&url,
reqwest::multipart::Form::new().text("duration", duration),
)
.send()
.await
} else {
self.client.request_post(&url).send().await
}
};
match request {
Ok(response) => match response.status() {
reqwest::StatusCode::OK => {
let Ok(auth_store) = response.json::<AuthStore>().await else {
return Err(ImpersonateError::UnexpectedResponse(
"Couldn't parse API response into Auth Data".to_string(),
));
};
let mut impersonate_client = PocketBase::new(&self.client.base_url());
impersonate_client.update_auth_store(auth_store);
Ok(impersonate_client)
}
reqwest::StatusCode::BAD_REQUEST => Err(ImpersonateError::BadRequest),
reqwest::StatusCode::UNAUTHORIZED => Err(ImpersonateError::Unauthorized),
reqwest::StatusCode::FORBIDDEN => Err(ImpersonateError::Forbidden),
reqwest::StatusCode::NOT_FOUND => Err(ImpersonateError::NotFound),
_ => Err(ImpersonateError::UnexpectedResponse(
response.status().to_string(),
)),
},
Err(error) => Err(ImpersonateError::Unreachable(error.to_string())),
}
}
}