1use oauth2::{
2 basic::{
3 BasicErrorResponse, BasicRevocationErrorResponse, BasicTokenIntrospectionResponse,
4 BasicTokenType,
5 },
6 reqwest::async_http_client,
7 AuthUrl, AuthorizationCode, Client, ClientId, ClientSecret, CsrfToken, ExtraTokenFields,
8 RedirectUrl, Scope, StandardRevocableToken, StandardTokenResponse, TokenResponse, TokenUrl,
9};
10use serde::{Deserialize, Serialize};
11use std::time::Duration;
12
13use crate::error::Error;
14
15#[derive(Serialize, Deserialize, Debug)]
16#[serde(rename_all = "snake_case")]
17pub enum OAuthScope {
18 #[serde(rename = "ads:read")]
19 AdsRead,
20 #[serde(rename = "ads:write")]
21 AdsWrite,
22 #[serde(rename = "billing:read")]
23 BillingRead,
24 #[serde(rename = "billing:write")]
25 BillingWrite,
26 #[serde(rename = "biz_access:read")]
27 BizAccessRead,
28 #[serde(rename = "biz_access:write")]
29 BizAccessWrite,
30 #[serde(rename = "boards:read")]
31 BoardsRead,
32 #[serde(rename = "boards:read_secret")]
33 BoardsReadSecret,
34 #[serde(rename = "boards:write")]
35 BoardsWrite,
36 #[serde(rename = "boards:write_secret")]
37 BoardsWriteSecret,
38 #[serde(rename = "catalogs:read")]
39 CatalogsRead,
40 #[serde(rename = "catalogs:write")]
41 CatalogsWrite,
42 #[serde(rename = "pins:read")]
43 PinsRead,
44 #[serde(rename = "pins:read_secret")]
45 PinsReadSecret,
46 #[serde(rename = "pins:write")]
47 PinsWrite,
48 #[serde(rename = "pins:write_secret")]
49 PinsWriteSecret,
50 #[serde(rename = "user_accounts:read")]
51 UserAccountsRead,
52 #[serde(rename = "user_accounts:write")]
53 UserAccountsWrite,
54}
55
56impl OAuthScope {
57 pub fn all() -> Vec<Self> {
58 vec![
59 Self::AdsRead,
60 Self::AdsWrite,
61 Self::BillingRead,
62 Self::BillingWrite,
63 Self::BizAccessRead,
64 Self::BizAccessWrite,
65 Self::BoardsRead,
66 Self::BoardsReadSecret,
67 Self::BoardsWrite,
68 Self::BoardsWriteSecret,
69 Self::CatalogsRead,
70 Self::CatalogsWrite,
71 Self::PinsRead,
72 Self::PinsReadSecret,
73 Self::PinsWrite,
74 Self::PinsWriteSecret,
75 Self::UserAccountsRead,
76 Self::UserAccountsWrite,
77 ]
78 }
79}
80
81impl std::fmt::Display for OAuthScope {
82 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
83 match self {
84 Self::AdsRead => write!(f, "ads:read"),
85 Self::AdsWrite => write!(f, "ads:write"),
86 Self::BillingRead => write!(f, "billing:read"),
87 Self::BillingWrite => write!(f, "billing:write"),
88 Self::BizAccessRead => write!(f, "biz_access:read"),
89 Self::BizAccessWrite => write!(f, "biz_access:write"),
90 Self::BoardsRead => write!(f, "boards:read"),
91 Self::BoardsReadSecret => write!(f, "boards:read_secret"),
92 Self::BoardsWrite => write!(f, "boards:write"),
93 Self::BoardsWriteSecret => write!(f, "boards:write_secret"),
94 Self::CatalogsRead => write!(f, "catalogs:read"),
95 Self::CatalogsWrite => write!(f, "catalogs:write"),
96 Self::PinsRead => write!(f, "pins:read"),
97 Self::PinsReadSecret => write!(f, "pins:read_secret"),
98 Self::PinsWrite => write!(f, "pins:write"),
99 Self::PinsWriteSecret => write!(f, "pins:write_secret"),
100 Self::UserAccountsRead => write!(f, "user_accounts:read"),
101 Self::UserAccountsWrite => write!(f, "user_accounts:write"),
102 }
103 }
104}
105
106const AUTH_URL: &str = "https://www.pinterest.com/oauth/";
107const TOKEN_URL: &str = "https://api.pinterest.com/v5/oauth/token";
108
109#[derive(Debug, Clone)]
110pub struct OAuthUrlResult {
111 pub oauth_url: String,
112 pub pkce_verifier: String,
113}
114
115#[derive(Debug, Clone, Default)]
116pub struct TokenResult {
117 pub access_token: String,
118 pub refresh_token: Option<String>,
119 pub token_type: String,
120 pub extra: InnerExtraTokenFields,
121 pub expires_in: Option<Duration>,
122}
123
124#[derive(Debug, Clone, Default, Deserialize, Serialize)]
125pub struct InnerExtraTokenFields {
126 pub response_type: String,
127 pub refresh_token_expires_in: Option<u64>,
128 pub refresh_token_expires_at: Option<u64>,
129}
130impl ExtraTokenFields for InnerExtraTokenFields {}
131
132pub struct Oauth {
133 basic_client: Client<
134 BasicErrorResponse,
135 StandardTokenResponse<InnerExtraTokenFields, BasicTokenType>,
136 BasicTokenType,
137 BasicTokenIntrospectionResponse,
138 StandardRevocableToken,
139 BasicRevocationErrorResponse,
140 >,
141 redirect_url: RedirectUrl,
142 scopes: Vec<Scope>,
143}
144
145impl Oauth {
146 pub fn new(
147 api_key_code: &str,
148 api_secret_code: &str,
149 callback_url: &str,
150 scopes: Vec<OAuthScope>,
151 ) -> Result<Self, Error> {
152 let basic_client = Client::new(
153 ClientId::new(api_key_code.to_owned()),
154 Some(ClientSecret::new(api_secret_code.to_owned())),
155 AuthUrl::new(AUTH_URL.to_owned())?,
156 Some(TokenUrl::new(TOKEN_URL.to_owned())?),
157 );
158 let redirect_url = RedirectUrl::new(callback_url.to_string())?;
159 let scopes: Vec<Scope> = scopes
160 .into_iter()
161 .map(|it| Scope::new(it.to_string()))
162 .collect();
163 Ok(Self {
164 basic_client,
165 redirect_url,
166 scopes,
167 })
168 }
169
170 pub fn oauth_url(&self, state: Option<String>) -> OAuthUrlResult {
171 let (pkce_challenge, pkce_verifier) = oauth2::PkceCodeChallenge::new_random_sha256();
172 let csrf_token = match state {
173 Some(ref state_value) => CsrfToken::new(state_value.clone()),
174 None => CsrfToken::new_random(),
175 };
176 let (auth_url, _csrf_token) = self
177 .basic_client
178 .clone()
179 .set_redirect_uri(self.redirect_url.clone())
180 .authorize_url(|| csrf_token)
181 .add_scopes(self.scopes.clone())
182 .set_pkce_challenge(pkce_challenge)
183 .url();
184
185 OAuthUrlResult {
186 oauth_url: auth_url.to_string(),
187 pkce_verifier: pkce_verifier.secret().to_string(),
188 }
189 }
190
191 pub async fn token(&self, pkce_verifier_str: &str, code: &str) -> Result<TokenResult, Error> {
192 let pkce_verifier = oauth2::PkceCodeVerifier::new(pkce_verifier_str.to_owned());
193
194 let token = self
195 .basic_client
196 .clone()
197 .set_redirect_uri(self.redirect_url.clone())
198 .exchange_code(AuthorizationCode::new(code.to_owned()))
199 .set_pkce_verifier(pkce_verifier)
200 .request_async(async_http_client)
201 .await
202 .map_err(|e| Error::Oauth(format!("{:?}", e)))?;
203 Ok(TokenResult {
204 access_token: token.access_token().secret().to_string(),
205 token_type: token.token_type().as_ref().to_string(),
206 refresh_token: token.refresh_token().map(|it| it.secret().to_string()),
207 expires_in: token.expires_in(),
208 extra: token.extra_fields().clone(),
209 })
210 }
211}