summaryrefslogtreecommitdiff
path: root/src/auth.rs
diff options
context:
space:
mode:
authorHampusM <hampus@hampusmat.com>2022-09-09 20:03:02 +0200
committerHampusM <hampus@hampusmat.com>2022-09-09 20:04:54 +0200
commitdb42316544c65951c7781357ec5aaba1b9abb8ab (patch)
tree13fd19efb80cf29f054c1760e8580f4379ce6a6a /src/auth.rs
parent8a02d3386d4ce0b58de943fcf42bd072af1e0b42 (diff)
refactor: make AuthPromptHandler use the Hyper web server
Diffstat (limited to 'src/auth.rs')
-rw-r--r--src/auth.rs154
1 files changed, 154 insertions, 0 deletions
diff --git a/src/auth.rs b/src/auth.rs
new file mode 100644
index 0000000..4af4997
--- /dev/null
+++ b/src/auth.rs
@@ -0,0 +1,154 @@
+//! Deezer API authentication.
+use std::convert::Infallible;
+use std::error::Error;
+use std::fmt::Display;
+use std::net::ToSocketAddrs;
+use std::time::Duration;
+
+use hyper::service::{make_service_fn, service_fn};
+use hyper::{Body, Request, Response, Server};
+use serde::Deserialize;
+use tokio::sync::mpsc;
+use tokio::task::JoinHandle;
+use tokio::{select, spawn};
+
+use crate::errors::auth::AuthPromptHandlerError;
+
+const AUTH_URL: &str = "https://connect.deezer.com/oauth/auth.php";
+
+/// A Deezer access token.
+#[derive(Debug, Clone)]
+pub struct AccessToken
+{
+ /// The access token.
+ pub access_token: String,
+
+ /// The duration until the access token expires.
+ pub expires: Duration,
+}
+
+/// 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 addr = format!("{}:{}", address, port)
+ .to_socket_addrs()
+ .map_err(|_| AuthPromptHandlerError::InvalidAddress)?
+ .next()
+ .map_or_else(|| Err(AuthPromptHandlerError::InvalidAddress), Ok)?;
+
+ let make_service = make_service_fn(move |_| {
+ let done_tx_clone = done_tx.clone();
+
+ let service =
+ service_fn(move |req| handle_auth_code(done_tx_clone.clone(), req));
+
+ async move { Ok::<_, Infallible>(service) }
+ });
+
+ let server = Server::bind(&addr).serve(make_service);
+
+ let handle = spawn(async move {
+ let opt_auth_code = select! {
+ result = server => {
+ 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,
+ })
+ }
+}
+
+#[derive(Debug, Deserialize)]
+struct AuthCodeQuery
+{
+ error_reason: Option<String>,
+ code: Option<AuthCode>,
+}
+
+async fn handle_auth_code(
+ done_tx: mpsc::Sender<AuthCode>,
+ request: Request<Body>,
+) -> Result<Response<Body>, String>
+{
+ let query = serde_urlencoded::from_str::<AuthCodeQuery>(
+ request.uri().query().map_or_else(|| "", |query| query),
+ )
+ .map_err(|err| format!("Invalid query. {}", err))?;
+
+ if let Some(error_reason) = &query.error_reason {
+ return Ok(Response::builder().status(401).body(Body::from(format!(
+ "Error: No authentication code was retrieved. Reason: {}\n\nYou can close this tab",
+ error_reason
+ ))).unwrap());
+ }
+
+ let auth_code = match &query.code {
+ Some(auth_code) => auth_code,
+ None => {
+ return Ok(Response::builder()
+ .status(400)
+ .body(Body::from("Error: No authentication code was retrieved."))
+ .unwrap());
+ }
+ };
+
+ done_tx.send(auth_code.clone()).await.unwrap();
+
+ Ok(Response::builder()
+ .status(200)
+ .body(Body::from(
+ "Authentication code was successfully retrieved.\n\nYou can close this tab",
+ ))
+ .unwrap())
+}