sfr-server 0.1.1

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 axum::async_trait;
use sc::{OauthRedirectQuery, OauthV2AccessResponse, Slack};
use ssa::OauthClient;
use st::OauthV2AccessRequest;

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

    /// Returns OAuth access_token from team_id (An ID of Workspace).
    async fn take_oauth_token_from_team_id(&self, team_id: &str) -> Result<String, ResponseError>;

    /// 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.
    async fn treat_oauth_redirect(
        &self,
        client: reqwest::Client,
        slack: &Slack,
        code: &str,
    ) -> Result<OauthV2AccessResponse, ResponseError> {
        let client = OauthClient::new(client);
        let form = self.oauth_v2_access_request_form(slack, code);
        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 async fn handle(
        &self,
        client: reqwest::Client,
        slack: Slack,
        query: OauthRedirectQuery,
    ) -> Result<impl axum::response::IntoResponse, ResponseError> {
        tracing::info!("oauth redirect: start to process");

        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
    }
}