sfr-server 0.1.2

The server implementation for a Slack App.
Documentation
//! The trait representing the interface that treats about OAuth.

use sfr_core as sc;
use sfr_slack_api as ssa;
use sfr_types as st;

use crate::{OauthRedirectResponse, ResponseError};
use sc::{OauthRedirectQuery, OauthV2AccessResponse, Slack};
use ssa::OauthClient;
use st::OauthV2AccessRequest;

/// The trait representing the interface that treats about OAuth.
pub trait OauthHandlerTrait: Send + Sync {
    /// Handles [`OauthV2AccessResponse`].
    ///
    /// If this handler returns `None`, the framework will return default value.
    fn handle_oauth(
        &self,
        client: reqwest::Client,
        body: OauthV2AccessResponse,
    ) -> impl std::future::Future<Output = Result<Option<OauthRedirectResponse>, ResponseError>> + Send;

    /// Returns OAuth access_token from team_id (An ID of Workspace).
    fn take_oauth_token_from_team_id(
        &self,
        team_id: &str,
    ) -> impl std::future::Future<Output = Result<String, ResponseError>> + Send;

    /// Returns `grant_type` to create [`OauthV2AccessRequest`] by [`OauthHandlerTrait::oauth_v2_access_request_form`].
    fn grant_type(&self) -> &str {
        "authorization_code"
    }
    /// Returns `redirect_uri` to create [`OauthV2AccessRequest`] by [`OauthHandlerTrait::oauth_v2_access_request_form`].
    fn redirect_uri(&self) -> &str;

    /// Creates and returns [`OauthV2AccessRequest`].
    fn oauth_v2_access_request_form<'a>(
        &'a self,
        slack: &'a Slack,
        code: &'a str,
    ) -> OauthV2AccessRequest<'a> {
        OauthV2AccessRequest {
            client_id: slack.client_id(),
            client_secret: slack.client_secret(),
            grant_type: self.grant_type(),
            redirect_uri: self.redirect_uri(),
            code,
        }
    }

    /// Treats redirect request.
    fn treat_oauth_redirect<'a>(
        &'a self,
        client: reqwest::Client,
        slack: &'a Slack,
        code: &'a str,
    ) -> impl std::future::Future<Output = Result<OauthV2AccessResponse, ResponseError>> + Send + 'a
    {
        let client = OauthClient::new(client);
        let form = self.oauth_v2_access_request_form(slack, code);
        async move {
            let resp = client
                .oauth_v2_access(form)
                .await
                .map_err(|e| ResponseError::InternalServerError(Box::new(e)))?;
            Ok(resp)
        }
    }
}

/// The new type to wrap [`OauthHandlerTrait`].
#[derive(Clone)]
pub(crate) struct OauthHandler<T>(T)
where
    T: OauthHandlerTrait;

impl<T> OauthHandler<T>
where
    T: OauthHandlerTrait,
{
    /// The Constructor.
    pub fn new(inner: T) -> Self {
        Self(inner)
    }

    /// The wrapper function for a OAuth.
    pub fn handle<'a>(
        &'a self,
        client: reqwest::Client,
        slack: Slack,
        query: OauthRedirectQuery,
    ) -> impl std::future::Future<Output = Result<impl axum::response::IntoResponse, ResponseError>>
           + Send
           + 'a {
        tracing::info!("oauth redirect: start to process");

        async move {
            let body = self
                .0
                .treat_oauth_redirect(client.clone(), &slack, query.code())
                .await?;

            let resp = self
                .0
                .handle_oauth(client, body)
                .await?
                .unwrap_or_else(default_redirect_response);

            Ok(resp)
        }
    }
}

/// The default OAuth redirect response.
fn default_redirect_response() -> OauthRedirectResponse {
    #[allow(clippy::missing_docs_in_private_items)] // https://github.com/rust-lang/rust-clippy/issues/13298
    const DEFAULT_RESPONSE: &str = r#"<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title></title></head><body>ok</body></html>"#;

    OauthRedirectResponse::html(DEFAULT_RESPONSE.into())
}

impl<T> std::ops::Deref for OauthHandler<T>
where
    T: OauthHandlerTrait,
{
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}