summaryrefslogtreecommitdiff
path: root/src/auth
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
parentafade48668042eeb0740c7c5a1cc3806baaedc94 (diff)
feat: add authentication
Diffstat (limited to 'src/auth')
-rw-r--r--src/auth/mod.rs145
-rw-r--r--src/auth/service.rs40
2 files changed, 185 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),
+ }
+ }
+}
diff --git a/src/auth/service.rs b/src/auth/service.rs
new file mode 100644
index 0000000..b9b44d4
--- /dev/null
+++ b/src/auth/service.rs
@@ -0,0 +1,40 @@
+use actix_web::web::{Data, Query};
+use actix_web::{get, HttpResponse};
+use serde::Deserialize;
+use tokio::sync::mpsc;
+
+use crate::auth::AuthCode;
+
+#[derive(Debug, Deserialize)]
+struct AuthCodeQuery
+{
+ error_reason: Option<String>,
+ code: Option<AuthCode>,
+}
+
+#[get("/")]
+async fn retrieve_auth_code(
+ query: Query<AuthCodeQuery>,
+ done_tx: Data<mpsc::Sender<AuthCode>>,
+) -> HttpResponse
+{
+ if let Some(error_reason) = &query.error_reason {
+ return HttpResponse::Unauthorized().body(format!(
+ "Error: No authentication code was retrieved. Reason: {}\n\nYou can close this tab",
+ error_reason
+ ));
+ }
+
+ let auth_code = match &query.code {
+ Some(auth_code) => auth_code,
+ None => {
+ return HttpResponse::BadRequest()
+ .body("Error: No authentication code was retrieved");
+ }
+ };
+
+ done_tx.send(auth_code.clone()).await.unwrap();
+
+ HttpResponse::Ok()
+ .body("Authentication code was successfully retrieved.\n\nYou can close this tab")
+}