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}