1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
//! 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
    }
}