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#[derive(Clone, Debug)]
13pub struct OAuthSvc(pub(crate) Arc<Config>);
14
15impl OAuthSvc {
16 #[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 #[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}