//! 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>>, } 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 { let (done_tx, mut done_rx) = mpsc::channel::(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. #[derive(Debug)] 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 { let response = reqwest::get(format!( "{}?app_id={}&secret={}&code={}&output=json", ACCESS_TOKEN_URL, app_id, secret_key, auth_code )) .await? .json::() .await?; Ok(response.into()) } #[derive(Debug, Deserialize)] struct AccessTokenResponse { pub access_token: String, pub expires: u64, } impl From for AccessToken { fn from(response: AccessTokenResponse) -> Self { Self { access_token: response.access_token, expires: Duration::from_secs(response.expires), } } }