Skip to main content

resend_rs/
oauth.rs

1use std::sync::Arc;
2
3use reqwest::Method;
4
5use crate::{
6    Config, Result,
7    list_opts::{ListOptions, ListResponse},
8    types::{OAuthGrant, RevokeOAuthGrantResponse},
9};
10
11/// `Resend` APIs for `/oauth` endpoints.
12#[derive(Clone, Debug)]
13pub struct OAuthSvc(pub(crate) Arc<Config>);
14
15impl OAuthSvc {
16    /// Retrieve a list of OAuth grants for the authenticated team.
17    ///
18    /// - Default limit: *infinite*
19    ///
20    /// <https://resend.com/docs/api-reference/oauth/list-grants>
21    #[maybe_async::maybe_async]
22    pub async fn list<T>(&self, list_opts: ListOptions<T>) -> Result<ListResponse<OAuthGrant>> {
23        let request = self.0.build(Method::GET, "/oauth/grants").query(&list_opts);
24        let response = self.0.send(request).await?;
25        let content = response.json::<ListResponse<OAuthGrant>>().await?;
26
27        Ok(content)
28    }
29
30    /// Revoke an OAuth grant for the authenticated team.
31    ///
32    /// <https://resend.com/docs/api-reference/oauth/revoke-grant>
33    #[maybe_async::maybe_async]
34    pub async fn revoke(&self, oauth_grant_id: &str) -> Result<RevokeOAuthGrantResponse> {
35        let path = format!("/oauth/grants/{oauth_grant_id}");
36
37        let request = self.0.build(Method::DELETE, &path);
38        let response = self.0.send(request).await?;
39        let content = response.json::<RevokeOAuthGrantResponse>().await?;
40
41        Ok(content)
42    }
43}
44
45#[allow(unreachable_pub)]
46pub mod types {
47    use serde::{Deserialize, Serialize};
48
49    crate::define_id_type!(OAuthGrantId);
50    crate::define_id_type!(ClientId);
51
52    #[must_use]
53    #[derive(Debug, Clone, Serialize, Deserialize)]
54    pub struct OAuthGrant {
55        pub id: OAuthGrantId,
56        pub client_id: ClientId,
57        pub scopes: Vec<String>,
58        pub created_at: String,
59        pub revoked_at: Option<String>,
60        pub revoked_reason: Option<String>,
61        pub client: OAuthGrantClient,
62    }
63
64    #[must_use]
65    #[derive(Debug, Clone, Serialize, Deserialize)]
66    pub struct OAuthGrantClient {
67        pub name: String,
68        pub logo_uri: Option<String>,
69    }
70
71    #[must_use]
72    #[derive(Debug, Clone, Serialize, Deserialize)]
73    pub struct RevokeOAuthGrantResponse {
74        pub id: OAuthGrantId,
75        pub revoked_at: String,
76        pub revoked_reason: String,
77    }
78}
79
80#[cfg(test)]
81#[allow(clippy::unwrap_used)]
82mod test {
83    #[cfg(not(feature = "blocking"))]
84    use crate::{
85        list_opts::ListOptions,
86        test::{CLIENT, DebugResult},
87    };
88    use crate::{
89        list_opts::ListResponse,
90        types::{OAuthGrant, RevokeOAuthGrantResponse},
91    };
92
93    #[tokio_shared_rt::test(shared = true)]
94    #[serial_test::serial]
95    #[cfg(not(feature = "blocking"))]
96    async fn all() -> DebugResult<()> {
97        let resend = &*CLIENT;
98
99        let logs = resend.oauth.list(ListOptions::default()).await?;
100        assert!(logs.data.is_empty());
101
102        Ok(())
103    }
104
105    #[test]
106    fn deserialize_grant() {
107        let grant = r#"{
108          "id": "650e8400-e29b-41d4-a716-446655440002",
109          "client_id": "430eed87-632a-4ea6-90db-0aace67ec228",
110          "scopes": ["emails:send", "domains:read"],
111          "created_at": "2026-04-07 00:11:13.110779+00",
112          "revoked_at": "2026-04-09 00:11:13.110779+00",
113          "revoked_reason": "revoked_from_api",
114          "client": {
115            "name": "Resend CLI",
116            "logo_uri": "https://example.com/logo.png"
117          }
118        }"#;
119
120        let res = serde_json::from_str::<OAuthGrant>(grant);
121        assert!(res.is_ok());
122    }
123
124    #[test]
125    fn deserialize_list() {
126        let grants = r#"{
127          "object": "list",
128          "has_more": false,
129          "data": [
130            {
131              "id": "650e8400-e29b-41d4-a716-446655440001",
132              "client_id": "430eed87-632a-4ea6-90db-0aace67ec228",
133              "scopes": ["emails:send"],
134              "created_at": "2026-04-08 00:11:13.110779+00",
135              "revoked_at": null,
136              "revoked_reason": null,
137              "client": {
138                "name": "Resend CLI",
139                "logo_uri": "https://example.com/logo.png"
140              }
141            },
142            {
143              "id": "650e8400-e29b-41d4-a716-446655440002",
144              "client_id": "430eed87-632a-4ea6-90db-0aace67ec228",
145              "scopes": ["emails:send", "domains:read"],
146              "created_at": "2026-04-07 00:11:13.110779+00",
147              "revoked_at": "2026-04-09 00:11:13.110779+00",
148              "revoked_reason": "revoked_from_api",
149              "client": {
150                "name": "Resend CLI",
151                "logo_uri": "https://example.com/logo.png"
152              }
153            }
154          ]
155        }"#;
156
157        let res = serde_json::from_str::<ListResponse<OAuthGrant>>(grants);
158        assert!(res.is_ok(), "{:?}", res.err());
159    }
160
161    #[test]
162    fn deserialize_revoke() {
163        let revoke = r#"{
164          "object": "oauth_grant",
165          "id": "650e8400-e29b-41d4-a716-446655440001",
166          "revoked_at": "2026-04-08T00:11:13.110Z",
167          "revoked_reason": "revoked_from_api"
168        }"#;
169
170        let res = serde_json::from_str::<RevokeOAuthGrantResponse>(revoke);
171        assert!(res.is_ok());
172    }
173}