1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
|
//! Deezer API authentication.
use std::error::Error;
use std::fmt::Display;
use std::time::Duration;
use actix_web::web::Data;
use actix_web::{App, HttpServer};
use serde::Deserialize;
use tokio::sync::mpsc;
use tokio::task::JoinHandle;
use tokio::{select, spawn};
use crate::auth::service::retrieve_auth_code;
use crate::errors::auth::{AccessTokenRequestError, AuthPromptHandlerError};
mod service;
const AUTH_URL: &str = "https://connect.deezer.com/oauth/auth.php";
const ACCESS_TOKEN_URL: &str = "https://connect.deezer.com/oauth/access_token.php";
/// A Deezer authentication code.
#[derive(Debug, Deserialize, Clone)]
pub struct AuthCode(String);
impl Display for AuthCode
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
{
self.0.fmt(f)
}
}
/// A Deezer authentication prompt handler.
pub struct AuthPromptHandler
{
/// URL to the Deezer authentication prompt.
pub auth_prompt_url: String,
/// Handle for the running authentication prompt handler.
///
/// Finishes when a single authentication has occurred.
pub handler: JoinHandle<Result<AuthCode, Box<dyn Error + Send + Sync>>>,
}
impl AuthPromptHandler
{
/// Runs a web server that handles a Deezer authentication prompt.
///
/// The argument `address` must be in the same domain as the domain you defined when
/// you created the application with the ID `app_id`.
///
/// # Errors
/// Will return Err if the web server created is unable to bind to `address`.
pub async fn run(
app_id: u32,
address: String,
port: u16,
uri_scheme: String,
) -> Result<Self, AuthPromptHandlerError>
{
let (done_tx, mut done_rx) = mpsc::channel::<AuthCode>(1);
let done_tx_data = Data::new(done_tx);
let server = HttpServer::new(move || {
App::new()
.app_data(done_tx_data.clone())
.service(retrieve_auth_code)
})
.bind((address.clone(), port))
.map_err(|_| AuthPromptHandlerError::BindAddressFailed)?;
let server_future = server.run();
let handle = spawn(async move {
let opt_auth_code = select! {
result = server_future => {
result.map(|_| None)
},
auth_code = async {
done_rx.recv().await
} => Ok(auth_code)
}?;
Ok(opt_auth_code.map_or_else(|| Err("No auth code was received"), Ok)?)
});
Ok(Self {
auth_prompt_url: format!(
"{}?app_id={}&redirect_uri={}://{}:{}&perms=basic_access",
AUTH_URL, app_id, uri_scheme, address, port
),
handler: handle,
})
}
}
/// A Deezer access token.
pub struct AccessToken
{
/// The access token.
pub access_token: String,
/// The duration until the access token expires.
pub expires: Duration,
}
/// Sends a request for a access token.
///
/// # Errors
/// Will return Err if either sending the request fails or parsing the response fails.
pub async fn request_access_token(
app_id: u32,
secret_key: String,
auth_code: AuthCode,
) -> Result<AccessToken, AccessTokenRequestError>
{
let response = reqwest::get(format!(
"{}?app_id={}&secret={}&code={}&output=json",
ACCESS_TOKEN_URL, app_id, secret_key, auth_code
))
.await?
.json::<AccessTokenResponse>()
.await?;
Ok(response.into())
}
#[derive(Debug, Deserialize)]
struct AccessTokenResponse
{
pub access_token: String,
pub expires: u64,
}
impl From<AccessTokenResponse> for AccessToken
{
fn from(response: AccessTokenResponse) -> Self
{
Self {
access_token: response.access_token,
expires: Duration::from_secs(response.expires),
}
}
}
|