sfr_server/handler/
oauth.rs

1//! The trait representing the interface that treats about OAuth.
2
3use sfr_core as sc;
4use sfr_slack_api as ssa;
5use sfr_types as st;
6
7use crate::{OauthRedirectResponse, ResponseError};
8use axum::async_trait;
9use sc::{OauthRedirectQuery, OauthV2AccessResponse, Slack};
10use ssa::OauthClient;
11use st::OauthV2AccessRequest;
12
13/// The trait representing the interface that treats about OAuth.
14#[async_trait]
15pub trait OauthHandlerTrait: Send + Sync {
16    /// Handles [`OauthV2AccessResponse`].
17    ///
18    /// If this handler returns `None`, the framework will return default value.
19    async fn handle_oauth(
20        &self,
21        client: reqwest::Client,
22        body: OauthV2AccessResponse,
23    ) -> Result<Option<OauthRedirectResponse>, ResponseError>;
24
25    /// Returns OAuth access_token from team_id (An ID of Workspace).
26    async fn take_oauth_token_from_team_id(&self, team_id: &str) -> Result<String, ResponseError>;
27
28    /// Returns `grant_type` to create [`OauthV2AccessRequest`] by [`OauthHandlerTrait::oauth_v2_access_request_form`].
29    fn grant_type(&self) -> &str {
30        "authorization_code"
31    }
32    /// Returns `redirect_uri` to create [`OauthV2AccessRequest`] by [`OauthHandlerTrait::oauth_v2_access_request_form`].
33    fn redirect_uri(&self) -> &str;
34
35    /// Creates and returns [`OauthV2AccessRequest`].
36    fn oauth_v2_access_request_form<'a>(
37        &'a self,
38        slack: &'a Slack,
39        code: &'a str,
40    ) -> OauthV2AccessRequest<'a> {
41        OauthV2AccessRequest {
42            client_id: slack.client_id(),
43            client_secret: slack.client_secret(),
44            grant_type: self.grant_type(),
45            redirect_uri: self.redirect_uri(),
46            code,
47        }
48    }
49
50    /// Treats redirect request.
51    async fn treat_oauth_redirect(
52        &self,
53        client: reqwest::Client,
54        slack: &Slack,
55        code: &str,
56    ) -> Result<OauthV2AccessResponse, ResponseError> {
57        let client = OauthClient::new(client);
58        let form = self.oauth_v2_access_request_form(slack, code);
59        let resp = client
60            .oauth_v2_access(form)
61            .await
62            .map_err(|e| ResponseError::InternalServerError(Box::new(e)))?;
63        Ok(resp)
64    }
65}
66
67/// The new type to wrap [`OauthHandlerTrait`].
68#[derive(Clone)]
69pub(crate) struct OauthHandler<T>(T)
70where
71    T: OauthHandlerTrait;
72
73impl<T> OauthHandler<T>
74where
75    T: OauthHandlerTrait,
76{
77    /// The Constructor.
78    pub fn new(inner: T) -> Self {
79        Self(inner)
80    }
81
82    /// The wrapper function for a OAuth.
83    pub async fn handle(
84        &self,
85        client: reqwest::Client,
86        slack: Slack,
87        query: OauthRedirectQuery,
88    ) -> Result<impl axum::response::IntoResponse, ResponseError> {
89        tracing::info!("oauth redirect: start to process");
90
91        let body = self
92            .0
93            .treat_oauth_redirect(client.clone(), &slack, query.code())
94            .await?;
95
96        let resp = self
97            .0
98            .handle_oauth(client, body)
99            .await?
100            .unwrap_or_else(default_redirect_response);
101
102        Ok(resp)
103    }
104}
105
106/// The default OAuth redirect response.
107fn default_redirect_response() -> OauthRedirectResponse {
108    #[allow(clippy::missing_docs_in_private_items)] // https://github.com/rust-lang/rust-clippy/issues/13298
109    const DEFAULT_RESPONSE: &str = r#"<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title></title></head><body>ok</body></html>"#;
110
111    OauthRedirectResponse::html(DEFAULT_RESPONSE.into())
112}
113
114impl<T> std::ops::Deref for OauthHandler<T>
115where
116    T: OauthHandlerTrait,
117{
118    type Target = T;
119
120    fn deref(&self) -> &Self::Target {
121        &self.0
122    }
123}