From 1a86d984df0bdb13ef3371d4c2c54a14b269a7bd Mon Sep 17 00:00:00 2001 From: banocean <47253870+banocean@users.noreply.github.com> Date: Mon, 15 Jan 2024 14:41:47 +0100 Subject: [PATCH] `/login` endpoint --- src/server/routes/login.rs | 139 +++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 src/server/routes/login.rs diff --git a/src/server/routes/login.rs b/src/server/routes/login.rs new file mode 100644 index 0000000..a9a2767 --- /dev/null +++ b/src/server/routes/login.rs @@ -0,0 +1,139 @@ +use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; +use futures_util::TryFutureExt; +use serde::{Deserialize, Serialize}; +use twilight_http::Client; +use twilight_model::user::CurrentUser; +use twilight_model::util::Timestamp; +use warp::{Filter, Reply}; +use warp::http::StatusCode; +use crate::{env_unwrap, reject, response_type, with_value}; +use crate::server::error::{Rejection, MapErrorIntoInternalRejection}; +use crate::server::session::{Authenticator, AuthorizationInformation, Sessions}; + +#[derive(Deserialize)] +struct Query { + code: String +} + +pub fn login(authenticator: Arc, sessions: Arc) -> response_type!() { + let with_authenticator = with_value!(authenticator); + let with_sessions = with_value!(sessions); + + let redirect_uri = env_unwrap!("REDIRECT_URI"); + let client_secret = env_unwrap!("CLIENT_SECRET"); + let client_id = env_unwrap!("CLIENT_ID"); + + let with_redirect_uri = with_value!(redirect_uri); + let with_client_secret = with_value!(client_secret); + let with_client_id = with_value!(client_id); + + warp::get() + .and(warp::path("login")) + .and(warp::query::()) + .and(with_authenticator) + .and(with_sessions) + .and(with_redirect_uri) + .and(with_client_secret) + .and(with_client_id) + .and_then(run) +} + +#[derive(Serialize)] +#[serde(tag = "grant_type")] +enum GrantType { + #[serde(rename = "authorization_code")] + AuthorizationCode { code: String }, + #[serde(rename = "refresh_token")] + RefreshToken { refresh_token: String } +} + +#[derive(Serialize)] +struct Data<'a> { + pub client_id: String, + pub client_secret: String, + #[serde(flatten)] + pub grant_type: GrantType, + pub redirect_uri: Option, + pub scope: Option<&'a str>, +} + +#[derive(Deserialize)] +struct PartialAuthorizationInformation { + pub access_token: Box, + pub token_type: Box, + pub refresh_token: Box, + pub expires_in: u64, +} + +const SCOPE: &str = "identify,guilds"; + +#[derive(Serialize)] +struct Response<'a> { + user: &'a CurrentUser, + token: &'a String +} + +async fn run( + query: Query, + authenticator: Arc, + sessions: Arc, + redirect_uri: String, + client_secret: String, + client_id: String, +) -> Result, warp::Rejection> { + let code = query.code; + let response = reqwest::Client::new() + .post("https://discord.com/api/oauth2/token") + .header("Content-Type", "application/x-www-form-urlencoded") + .body( + serde_urlencoded::to_string(Data { + client_id, + client_secret, + grant_type: GrantType::AuthorizationCode { code }, + redirect_uri: Some(redirect_uri), + scope: Some(SCOPE) + }).map_err(|err| Rejection::Internal(err.into()))? + ) + .send() + .await + .map_err(|err| reject!(Rejection::Internal(err.into())))?; + + if response.status() != StatusCode::OK { + return Ok(Box::new(warp::reply::with_status( + response.text().await.unwrap_or_else(|_| "Discord rejected the request".to_string()), + StatusCode::BAD_REQUEST + ))) + } + + let response: PartialAuthorizationInformation = response.json() + .await.map_rejection()?; + + let http = Arc::new(Client::new( + format!("{} {}", response.token_type, response.access_token) + )); + + let user = http.current_user().await.map_rejection()? + .model().await.map_rejection()?; + let token = authenticator.generate_token(user.id).map_rejection()?; + + let reply = warp::reply::json(&Response { + user: &user, + token: &token + }); + + let expires_at = SystemTime::now() + .duration_since(UNIX_EPOCH).map_rejection()?.as_secs() + response.expires_in; + + sessions.add(Arc::new(AuthorizationInformation { + access_token: response.access_token, + refresh_token: response.refresh_token, + expires: Timestamp::from_secs(expires_at as i64).map_rejection()?, + scopes: vec![], + user, + http, + })).await; + + + return Ok(Box::new(reply)) +} \ No newline at end of file