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