shopify_client/oauth/
mod.rs1pub mod types;
2
3pub use types::AccessTokenResponse;
4
5use crate::common::http::http_client;
6use crate::common::types::APIError;
7
8const TOKEN_EXCHANGE_GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:token-exchange";
9const ID_TOKEN_TYPE: &str = "urn:ietf:params:oauth:token-type:id_token";
10const OFFLINE_ACCESS_TOKEN_TYPE: &str = "urn:shopify:params:oauth:token-type:offline-access-token";
11
12pub async fn exchange_session_token(
13 shop_url: &str,
14 id_token: &str,
15 client_id: &str,
16 client_secret: &str,
17) -> Result<AccessTokenResponse, APIError> {
18 let body = serde_json::json!({
19 "client_id": client_id,
20 "client_secret": client_secret,
21 "grant_type": TOKEN_EXCHANGE_GRANT_TYPE,
22 "subject_token": id_token,
23 "subject_token_type": ID_TOKEN_TYPE,
24 "requested_token_type": OFFLINE_ACCESS_TOKEN_TYPE,
25 });
26
27 request_access_token(shop_url, &body).await
28}
29
30pub async fn exchange_code(
31 shop_url: &str,
32 code: &str,
33 client_id: &str,
34 client_secret: &str,
35) -> Result<AccessTokenResponse, APIError> {
36 let body = serde_json::json!({
37 "client_id": client_id,
38 "client_secret": client_secret,
39 "code": code,
40 });
41
42 request_access_token(shop_url, &body).await
43}
44
45async fn request_access_token(
46 shop_url: &str,
47 body: &serde_json::Value,
48) -> Result<AccessTokenResponse, APIError> {
49 let endpoint = format!(
50 "{}/admin/oauth/access_token",
51 shop_url.trim_end_matches('/')
52 );
53
54 let response = http_client()
55 .post(&endpoint)
56 .header("Content-Type", "application/json")
57 .json(body)
58 .send()
59 .await
60 .map_err(|_| APIError::NetworkError)?;
61
62 let status = response.status();
63 let response_text = response.text().await.map_err(|_| APIError::FailedToParse)?;
64
65 if !status.is_success() {
66 return Err(APIError::ServerError {
67 errors: format!("{}: {}", status, response_text),
68 });
69 }
70
71 serde_json::from_str::<AccessTokenResponse>(&response_text).map_err(|_| APIError::FailedToParse)
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 #[test]
79 fn deserialize_access_token_response_with_scope() {
80 let json = r#"{"access_token":"shpat_abc123","scope":"read_products,write_products"}"#;
81 let resp: AccessTokenResponse = serde_json::from_str(json).unwrap();
82 assert_eq!(resp.access_token, "shpat_abc123");
83 assert_eq!(resp.scope.as_deref(), Some("read_products,write_products"));
84 }
85
86 #[test]
87 fn deserialize_access_token_response_without_scope() {
88 let json = r#"{"access_token":"shpat_abc123"}"#;
89 let resp: AccessTokenResponse = serde_json::from_str(json).unwrap();
90 assert_eq!(resp.access_token, "shpat_abc123");
91 assert_eq!(resp.scope, None);
92 }
93}