summaryrefslogtreecommitdiff
path: root/src/auth/mod.rs
diff options
context:
space:
mode:
authorHampusM <hampus@hampusmat.com>2022-09-05 17:57:28 +0200
committerHampusM <hampus@hampusmat.com>2022-09-05 17:57:28 +0200
commitd7929e7e9fee879a28871c2195620869db291441 (patch)
tree4b2c9a6b3bf64144a994d36d0a522404548116f3 /src/auth/mod.rs
parentafade48668042eeb0740c7c5a1cc3806baaedc94 (diff)
feat: add authentication
Diffstat (limited to 'src/auth/mod.rs')
-rw-r--r--src/auth/mod.rs145
1 files changed, 145 insertions, 0 deletions
diff --git a/src/auth/mod.rs b/src/auth/mod.rs
new file mode 100644
index 0000000..c5beeb3
--- /dev/null
+++ b/src/auth/mod.rs
@@ -0,0 +1,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),
+ }
+ }
+}