From 98b4551b864e01a31eae3a0c85896eb58ced48c9 Mon Sep 17 00:00:00 2001 From: Antonios Barotsis Date: Fri, 3 Jul 2026 20:56:14 +0200 Subject: [PATCH 1/2] oauth list revoke --- CHANGELOG.md | 4 ++ src/client.rs | 6 +- src/lib.rs | 5 ++ src/oauth.rs | 176 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 src/oauth.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 4246281..1ce648e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ and this project adheres to ## Unreleased +### Added + +- `list` and `revoke` oauth endpoints + ### Changed - `Log.id` is now a new `LogId` type instead of a `String` diff --git a/src/client.rs b/src/client.rs index cb10caa..4a9382b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -11,6 +11,7 @@ use crate::{ config::Config, events::EventsSvc, logs::LogsSvc, + oauth::OAuthSvc, services::{AutomationsSvc, ReceivingSvc}, webhooks::WebhookSvc, }; @@ -56,6 +57,8 @@ pub struct Resend { pub automations: AutomationsSvc, /// `Resend` APIs for `/events` endpoints. pub events: EventsSvc, + /// `Resend` APIs for `/oauth` endpoints. + pub oauth: OAuthSvc, } impl Resend { @@ -110,7 +113,8 @@ impl Resend { webhooks: WebhookSvc(Arc::clone(&inner)), logs: LogsSvc(Arc::clone(&inner)), automations: AutomationsSvc(Arc::clone(&inner)), - events: EventsSvc(inner), + events: EventsSvc(Arc::clone(&inner)), + oauth: OAuthSvc(inner), } } diff --git a/src/lib.rs b/src/lib.rs index 147270e..ba652c6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -68,6 +68,7 @@ pub mod events; pub mod idempotent; pub mod list_opts; mod logs; +mod oauth; pub mod rate_limit; mod receiving; mod segments; @@ -86,6 +87,7 @@ pub mod services { pub use super::domains::DomainsSvc; pub use super::emails::EmailsSvc; pub use super::logs::LogsSvc; + pub use super::oauth::OAuthSvc; pub use super::receiving::ReceivingSvc; pub use super::segments::SegmentsSvc; pub use super::templates::TemplateSvc; @@ -146,6 +148,9 @@ pub mod types { UpdateEventResponse, }; pub use super::logs::types::{Log, LogId}; + pub use super::oauth::types::{ + ClientId, OAuthGrant, OAuthGrantClient, OAuthGrantId, RevokeOAuthGrantResponse, + }; pub use super::receiving::types::{ ForwardInboundEmailResponse, ForwardReceivingEmail, GetInboundEmailOptions, GetInboundEmailRaw, InboundAttachment, InboundAttachmentId, InboundEmail, diff --git a/src/oauth.rs b/src/oauth.rs new file mode 100644 index 0000000..ae741fb --- /dev/null +++ b/src/oauth.rs @@ -0,0 +1,176 @@ +use std::sync::Arc; + +use reqwest::Method; + +use crate::{ + Config, Result, + list_opts::{ListOptions, ListResponse}, + types::{OAuthGrant, RevokeOAuthGrantResponse}, +}; + +/// `Resend` APIs for `/oauth` endpoints. +#[derive(Clone, Debug)] +pub struct OAuthSvc(pub(crate) Arc); + +impl OAuthSvc { + /// Retrieve a list of OAuth grants for the authenticated team. + /// + /// - Default limit: *infinite* + /// + /// + #[maybe_async::maybe_async] + #[allow(clippy::needless_pass_by_value)] + pub async fn list(&self, list_opts: ListOptions) -> Result> { + let request = self.0.build(Method::GET, "/oauth/grants").query(&list_opts); + let response = self.0.send(request).await?; + let content = response.json::>().await?; + + Ok(content) + } + + /// Revoke an OAuth grant for the authenticated team. + /// + /// + #[maybe_async::maybe_async] + pub async fn revoke(&self, oauth_grant_id: &str) -> Result { + let path = format!("/oauth/grants/{oauth_grant_id}"); + + let request = self.0.build(Method::DELETE, &path); + let response = self.0.send(request).await?; + let content = response.json::().await?; + + Ok(content) + } +} + +#[allow(unreachable_pub)] +pub mod types { + use serde::{Deserialize, Serialize}; + + crate::define_id_type!(OAuthGrantId); + crate::define_id_type!(ClientId); + + #[must_use] + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct OAuthGrant { + pub id: OAuthGrantId, + pub client_id: ClientId, + pub scopes: Vec, + pub resource: Option, + pub created_at: String, + pub revoked_at: Option, + pub revoked_reason: Option, + pub client: OAuthGrantClient, + } + + #[must_use] + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct OAuthGrantClient { + pub name: String, + pub logo_uri: Option, + } + + #[must_use] + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct RevokeOAuthGrantResponse { + pub id: OAuthGrantId, + pub revoked_at: String, + pub revoked_reason: String, + } +} + +#[cfg(test)] +#[allow(clippy::unwrap_used)] +mod test { + use crate::{ + list_opts::{ListOptions, ListResponse}, + types::OAuthGrant, + }; + use crate::{ + test::{CLIENT, DebugResult}, + types::RevokeOAuthGrantResponse, + }; + + #[tokio_shared_rt::test(shared = true)] + #[cfg(not(feature = "blocking"))] + async fn all() -> DebugResult<()> { + let resend = &*CLIENT; + + let logs = resend.oauth.list(ListOptions::default()).await?; + assert!(logs.data.is_empty()); + + Ok(()) + } + + #[test] + fn deserialize_grant() { + let grant = r#"{ + "id": "650e8400-e29b-41d4-a716-446655440002", + "client_id": "430eed87-632a-4ea6-90db-0aace67ec228", + "scopes": ["emails:send", "domains:read"], + "resource": "https://api.resend.com", + "created_at": "2026-04-07 00:11:13.110779+00", + "revoked_at": "2026-04-09 00:11:13.110779+00", + "revoked_reason": "revoked_from_api", + "client": { + "name": "Resend CLI", + "logo_uri": "https://example.com/logo.png" + } + }"#; + + let res = serde_json::from_str::(grant); + assert!(res.is_ok()); + } + + #[test] + fn deserialize_list() { + let grants = r#"{ + "object": "list", + "has_more": false, + "data": [ + { + "id": "650e8400-e29b-41d4-a716-446655440001", + "client_id": "430eed87-632a-4ea6-90db-0aace67ec228", + "scopes": ["emails:send"], + "resource": null, + "created_at": "2026-04-08 00:11:13.110779+00", + "revoked_at": null, + "revoked_reason": null, + "client": { + "name": "Resend CLI", + "logo_uri": "https://example.com/logo.png" + } + }, + { + "id": "650e8400-e29b-41d4-a716-446655440002", + "client_id": "430eed87-632a-4ea6-90db-0aace67ec228", + "scopes": ["emails:send", "domains:read"], + "resource": "https://api.resend.com", + "created_at": "2026-04-07 00:11:13.110779+00", + "revoked_at": "2026-04-09 00:11:13.110779+00", + "revoked_reason": "revoked_from_api", + "client": { + "name": "Resend CLI", + "logo_uri": "https://example.com/logo.png" + } + } + ] + }"#; + + let res = serde_json::from_str::>(grants); + assert!(res.is_ok(), "{:?}", res.err()); + } + + #[test] + fn deserialize_revoke() { + let revoke = r#"{ + "object": "oauth_grant", + "id": "650e8400-e29b-41d4-a716-446655440001", + "revoked_at": "2026-04-08T00:11:13.110Z", + "revoked_reason": "revoked_from_api" + }"#; + + let res = serde_json::from_str::(revoke); + assert!(res.is_ok()); + } +} From b3ec9bcfc47d89a3e0457d42fff612ef13274a62 Mon Sep 17 00:00:00 2001 From: Antonios Barotsis Date: Sat, 4 Jul 2026 23:13:04 +0200 Subject: [PATCH 2/2] remove resource --- src/oauth.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/oauth.rs b/src/oauth.rs index ae741fb..72f7b7e 100644 --- a/src/oauth.rs +++ b/src/oauth.rs @@ -56,7 +56,6 @@ pub mod types { pub id: OAuthGrantId, pub client_id: ClientId, pub scopes: Vec, - pub resource: Option, pub created_at: String, pub revoked_at: Option, pub revoked_reason: Option, @@ -108,7 +107,6 @@ mod test { "id": "650e8400-e29b-41d4-a716-446655440002", "client_id": "430eed87-632a-4ea6-90db-0aace67ec228", "scopes": ["emails:send", "domains:read"], - "resource": "https://api.resend.com", "created_at": "2026-04-07 00:11:13.110779+00", "revoked_at": "2026-04-09 00:11:13.110779+00", "revoked_reason": "revoked_from_api", @@ -132,7 +130,6 @@ mod test { "id": "650e8400-e29b-41d4-a716-446655440001", "client_id": "430eed87-632a-4ea6-90db-0aace67ec228", "scopes": ["emails:send"], - "resource": null, "created_at": "2026-04-08 00:11:13.110779+00", "revoked_at": null, "revoked_reason": null, @@ -145,7 +142,6 @@ mod test { "id": "650e8400-e29b-41d4-a716-446655440002", "client_id": "430eed87-632a-4ea6-90db-0aace67ec228", "scopes": ["emails:send", "domains:read"], - "resource": "https://api.resend.com", "created_at": "2026-04-07 00:11:13.110779+00", "revoked_at": "2026-04-09 00:11:13.110779+00", "revoked_reason": "revoked_from_api",