diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml
index b13859f47..a118a4e78 100644
--- a/.idea/sqldialects.xml
+++ b/.idea/sqldialects.xml
@@ -2,5 +2,8 @@
+
+
+
\ No newline at end of file
diff --git a/migrations/20200928020509_states.sql b/migrations/20200928020509_states.sql
new file mode 100644
index 000000000..96e8e1408
--- /dev/null
+++ b/migrations/20200928020509_states.sql
@@ -0,0 +1,4 @@
+CREATE TABLE states (
+ id bigint PRIMARY KEY,
+ url varchar(500)
+);
\ No newline at end of file
diff --git a/migrations/20200928033759_edit-states.sql b/migrations/20200928033759_edit-states.sql
new file mode 100644
index 000000000..46fa24952
--- /dev/null
+++ b/migrations/20200928033759_edit-states.sql
@@ -0,0 +1,4 @@
+-- Add migration script here
+ALTER TABLE states
+
+ADD COLUMN expires timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP + interval '1 hour';
\ No newline at end of file
diff --git a/migrations/20200928053955_make-url-not-null.sql b/migrations/20200928053955_make-url-not-null.sql
new file mode 100644
index 000000000..8649f57f6
--- /dev/null
+++ b/migrations/20200928053955_make-url-not-null.sql
@@ -0,0 +1,3 @@
+-- Add migration script here
+ALTER TABLE states
+ALTER COLUMN url SET NOT NULL;
\ No newline at end of file
diff --git a/src/database/models/ids.rs b/src/database/models/ids.rs
index e2a3baccb..4265fb3ec 100644
--- a/src/database/models/ids.rs
+++ b/src/database/models/ids.rs
@@ -73,6 +73,13 @@ generate_ids!(
"SELECT EXISTS(SELECT 1 FROM team_members WHERE id=$1)",
TeamMemberId
);
+generate_ids!(
+ pub generate_state_id,
+ StateId,
+ 8,
+ "SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1)",
+ StateId
+);
#[derive(Copy, Clone, Debug, Type)]
#[sqlx(transparent)]
@@ -109,6 +116,10 @@ pub struct CategoryId(pub i32);
#[sqlx(transparent)]
pub struct FileId(pub i64);
+#[derive(Copy, Clone, Debug, Type)]
+#[sqlx(transparent)]
+pub struct StateId(pub i64);
+
use crate::models::ids;
impl From for ModId {
diff --git a/src/models/ids.rs b/src/models/ids.rs
index f40585d63..46d287b39 100644
--- a/src/models/ids.rs
+++ b/src/models/ids.rs
@@ -169,7 +169,7 @@ pub mod base62_impl {
output
}
- fn parse_base62(string: &str) -> Result {
+ pub fn parse_base62(string: &str) -> Result {
let mut num: u64 = 0;
for c in string.chars() {
let next_digit;
diff --git a/src/routes/auth.rs b/src/routes/auth.rs
index 80fdbd954..93906f5e8 100644
--- a/src/routes/auth.rs
+++ b/src/routes/auth.rs
@@ -1,13 +1,23 @@
use crate::models::error::ApiError;
use log::{info};
-use actix_web::web::{Query, ServiceConfig, scope};
+use actix_web::web::{Query, ServiceConfig, scope, Data};
use actix_web::{get, HttpResponse};
use actix_web::http::StatusCode;
use serde::{Deserialize, Serialize};
use thiserror::Error;
+use serde_json::Value;
+use crate::database::models::generate_state_id;
+use sqlx::postgres::PgPool;
+use crate::models::ids::base62_impl::{to_base62, parse_base62};
+use chrono::Utc;
+use crate::models::ids::{DecodingError};
pub fn config(cfg: &mut ServiceConfig) {
- cfg.service(auth_callback);
+ cfg.service(
+ scope("/auth/")
+ .service(auth_callback)
+ .service(init)
+ );
}
#[derive(Error, Debug)]
@@ -22,8 +32,11 @@ pub enum AuthorizationError {
SerDeError(#[from] serde_json::Error),
#[error("Error while communicating to GitHub OAuth2")]
GithubError(#[from] reqwest::Error),
+ #[error("Invalid Authentication credentials")]
+ InvalidCredentialsError,
+ #[error("Error while decoding Base62")]
+ DecodingError(#[from] DecodingError),
}
-// "https://github.com/login/oauth/authorize?client_id=3acffb2e808d16d4b226&redirect_uri=http%3A%2F%2Flocalhost%3A8000%2Fapi%2Fv1%2Fauthcallback"
impl actix_web::ResponseError for AuthorizationError {
fn status_code(&self) -> StatusCode {
match self {
@@ -32,6 +45,8 @@ impl actix_web::ResponseError for AuthorizationError {
AuthorizationError::DatabaseError(..) => StatusCode::INTERNAL_SERVER_ERROR,
AuthorizationError::SerDeError(..) => StatusCode::BAD_REQUEST,
AuthorizationError::GithubError(..) => StatusCode::FAILED_DEPENDENCY,
+ AuthorizationError::InvalidCredentialsError => StatusCode::UNAUTHORIZED,
+ AuthorizationError::DecodingError(..) => StatusCode::BAD_REQUEST,
}
}
@@ -43,16 +58,23 @@ impl actix_web::ResponseError for AuthorizationError {
AuthorizationError::DatabaseError(..) => "database_error",
AuthorizationError::SerDeError(..) => "invalid_input",
AuthorizationError::GithubError(..) => "github_error",
+ AuthorizationError::InvalidCredentialsError => "invalid_credentials",
+ AuthorizationError::DecodingError(..) => "decoding_error",
},
description: &self.to_string(),
})
}
}
+#[derive(Serialize, Deserialize)]
+pub struct AuthorizationInit {
+ pub url: String,
+}
+
#[derive(Serialize, Deserialize)]
pub struct Authorization {
pub code: String,
- pub state: Option,
+ pub state: String,
}
#[derive(Serialize, Deserialize)]
@@ -62,8 +84,80 @@ pub struct AccessToken {
pub token_type: String,
}
-#[get("authcallback")]
-pub async fn auth_callback(Query(info): Query) -> Result {
+#[derive(Serialize, Deserialize)]
+pub struct GitHubUser {
+ pub login: String,
+ pub id: usize,
+ pub node_id: String,
+ pub avatar_url: String,
+ pub gravatar_id: String,
+ pub url: String,
+ pub bio: String,
+}
+
+//http://localhost:8000/api/v1/auth/init?url=https%3A%2F%2Fmodrinth.com%2Fmods
+#[get("init")]
+pub async fn init(Query(info): Query, client: Data) -> Result {
+ let mut transaction = client.begin().await?;
+
+ let state = generate_state_id(&mut transaction).await?;
+
+ sqlx::query!(
+ "
+ INSERT INTO states (id, url)
+ VALUES ($1, $2)
+ ",
+ state.0,
+ info.url
+ )
+ .execute(&mut *transaction)
+ .await?;
+
+ transaction.commit().await?;
+
+ let client_id = dotenv::var("GITHUB_CLIENT_ID")?;
+ let url = format!("https://github.com/login/oauth/authorize?client_id={}&state={}&scope={}", client_id, to_base62(state.0 as u64), "%20repo%20read%3Aorg%20read%3Auser%20user%3Aemail");
+
+ Ok(HttpResponse::PermanentRedirect()
+ .header("Location", &*url)
+ .json(AuthorizationInit {
+ url,
+ }))
+}
+
+#[get("callback")]
+pub async fn auth_callback(Query(info): Query, client: Data) -> Result {
+ let mut transaction = client.begin().await?;
+ let state_id = parse_base62(&*info.state)?;
+
+ let result = sqlx::query!(
+ "
+ SELECT url,expires FROM states
+ WHERE id = $1
+ ",
+ state_id as i64
+ )
+ .fetch_one(&mut *transaction)
+ .await?;
+
+ let now = Utc::now();
+ let duration = result.expires.signed_duration_since(now);
+
+ info!("{:?}", duration.num_seconds());
+ if duration.num_seconds() < 0 {
+ return Err(AuthorizationError::InvalidCredentialsError);
+ }
+
+ sqlx::query!(
+ "
+ DELETE FROM states
+ WHERE id = $1
+ ",
+ state_id as i64
+ )
+ .execute(&mut *transaction)
+ .await?;
+
let client_id = dotenv::var("GITHUB_CLIENT_ID")?;
let client_secret = dotenv::var("GITHUB_CLIENT_SECRET")?;
@@ -72,7 +166,9 @@ pub async fn auth_callback(Query(info): Query) -> Result) -> Result