1use crate::{
3 auth_server_builder, error::Result, utils, AuthAction, AuthConfig, AuthUrlProvider, AuthUser,
4 GenericAuthAction,
5};
6use async_trait::async_trait;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use serde_with::{formats::CommaSeparator, serde_as, StringWithSeparator};
10use std::collections::HashMap;
11
12pub struct AuthorizationServer {
13 config: AuthConfig,
14}
15
16auth_server_builder!();
17
18impl AuthUrlProvider for AuthorizationServer {
19 type AuthRequest = AuthRequest;
20 type TokenRequest = GetTokenRequest;
21 type UserInfoRequest = GetUserInfoRequest;
22
23 fn authorize_url(request: Self::AuthRequest) -> Result<String> {
24 let query = serde_urlencoded::to_string(request)?;
25 Ok(format!(
26 "https://graph.qq.com/oauth2.0/authorize?response_type=token&{query}"
27 ))
28 }
29
30 fn access_token_url(request: Self::TokenRequest) -> Result<String> {
31 let query = serde_urlencoded::to_string(request)?;
32 Ok(format!(
33 "https://graph.qq.com/oauth2.0/token?grant_type=authorization_code&{query}"
34 ))
35 }
36
37 fn user_info_url(request: Self::UserInfoRequest) -> Result<String> {
38 let query = serde_urlencoded::to_string(request)?;
39 Ok(format!("https://graph.qq.com/user/get_user_info?{query}"))
40 }
41}
42
43#[async_trait]
44impl AuthAction for AuthorizationServer {
45 type AuthCallback = AuthCallback;
46 type AuthToken = TokenResponse;
47 type AuthUser = UserInfoResponse;
48
49 async fn get_access_token(&self, callback: Self::AuthCallback) -> Result<Self::AuthToken> {
50 let AuthConfig {
51 client_id,
52 client_secret,
53 redirect_uri,
54 ..
55 } = &self.config;
56 let access_token_url = Self::access_token_url(GetTokenRequest {
57 client_id: client_id.to_string(),
58 client_secret: client_secret.clone().expect("client_secret is empty"),
59 code: callback.code,
60 redirect_uri: redirect_uri.to_string(),
61 fmt: Some(ResponseFormat::Json),
62 })?;
63 Ok(reqwest::get(access_token_url).await?.json().await?)
64 }
65
66 async fn get_user_info(&self, token: Self::AuthToken) -> Result<Self::AuthUser> {
67 let AuthConfig { client_id, .. } = &self.config;
68 let access_token = token.access_token;
69 let value = self.get_open_id(&access_token).await?;
70 let user_info_url = Self::user_info_url(GetUserInfoRequest {
71 openid: value.openid,
72 access_token: access_token,
73 oauth_consumer_key: client_id.to_string(),
74 })?;
75 Ok(reqwest::get(user_info_url).await?.json().await?)
76 }
77}
78
79#[async_trait]
80impl GenericAuthAction for AuthorizationServer {
81 async fn authorize<S: Into<String> + Send>(&self, state: S) -> Result<String> {
82 let AuthConfig {
83 client_id,
84 redirect_uri,
85 scope,
86 ..
87 } = &self.config;
88 Self::authorize_url(AuthRequest {
89 client_id: client_id.to_string(),
90 redirect_uri: redirect_uri.to_string(),
91 state: state.into(),
92 scope: scope.clone().or_else(|| Some(vec!["get_user_info".into()])),
93 ..Default::default()
94 })
95 }
96
97 async fn login<S: Into<String> + Send>(&self, callback: S) -> Result<AuthUser> {
98 let callback: AuthCallback = serde_urlencoded::from_str(&callback.into())?;
99 let AuthConfig { client_id, .. } = &self.config;
100 let token = self.get_access_token(callback).await?;
101 let access_token = token.access_token;
102 let open_id = self.get_open_id(&access_token).await?;
103 let user_info_url = Self::user_info_url(GetUserInfoRequest {
104 openid: open_id.openid.clone(),
105 access_token: access_token.clone(),
106 oauth_consumer_key: client_id.to_string(),
107 })?;
108 let user: UserInfoResponse = reqwest::get(user_info_url).await?.json().await?;
109 Ok(AuthUser {
110 user_id: open_id.openid,
111 name: user.nickname,
112 access_token: access_token,
113 refresh_token: token.refresh_token,
114 expires_in: token.expires_in.into(),
115 extra: user.extra,
116 })
117 }
118}
119
120impl AuthorizationServer {
121 async fn get_open_id(&self, access_token: &str) -> Result<OpenIdResp> {
122 let jsonp = reqwest::get(format!(
123 "https://graph.qq.com/oauth2.0/me?access_token={access_token}"
124 ))
125 .await?
126 .text()
127 .await?;
128 let json =
129 utils::substr_between(&jsonp, "callback(", ");").expect("jsonp response is valid");
130 Ok(serde_json::from_str(json)?)
131 }
132}
133
134#[serde_as]
135#[derive(Debug, Default, Serialize, Deserialize)]
136pub struct AuthRequest {
137 client_id: String,
138 redirect_uri: String,
139 state: String,
140 #[serde_as(as = "Option<StringWithSeparator::<CommaSeparator, String>>")]
141 scope: Option<Vec<String>>,
142 display: Option<QQDisplayStyle>,
143}
144
145#[derive(Debug, Serialize, Deserialize)]
146#[serde(rename_all = "lowercase")]
147pub enum QQDisplayStyle {
148 PC,
149 Mobile,
150}
151
152#[derive(Debug, Serialize, Deserialize)]
153pub struct AuthCallback {
154 code: String,
155 state: String,
156}
157
158#[derive(Debug, Serialize, Deserialize)]
159pub struct GetTokenRequest {
160 client_id: String,
161 client_secret: String,
162 code: String,
163 redirect_uri: String,
164 fmt: Option<ResponseFormat>,
165}
166
167#[derive(Debug, Serialize, Deserialize)]
168pub enum ResponseFormat {
169 #[serde(rename = "x-www-form-urlencoded")]
170 UrlEncoded,
171 #[serde(rename = "json")]
172 Json,
173}
174
175#[derive(Debug, Serialize, Deserialize)]
176pub struct RefreshTokenRequest {
177 grant_type: String,
178 client_id: String,
179 client_secret: String,
180 refresh_token: String,
181 fmt: Option<ResponseFormat>,
182}
183
184#[derive(Debug, Serialize, Deserialize)]
185pub struct TokenResponse {
186 access_token: String,
187 expires_in: i32,
188 refresh_token: String,
189}
190
191#[derive(Debug, Serialize, Deserialize)]
192pub struct OpenIdResp {
193 client_id: String,
194 openid: String,
195}
196
197#[derive(Debug, Serialize, Deserialize)]
198pub struct GetUserInfoRequest {
199 access_token: String,
200 oauth_consumer_key: String,
201 openid: String,
202}
203
204#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
206#[serde(rename_all = "camelCase")]
207pub struct UserInfoResponse {
208 pub nickname: String,
209 #[serde(flatten)]
210 pub extra: HashMap<String, Value>,
211}