You've already forked AstralRinth
forked from didirus/AstralRinth
Authentication workflow complete, add database link
This commit is contained in:
3
.idea/sqldialects.xml
generated
3
.idea/sqldialects.xml
generated
@@ -2,5 +2,8 @@
|
|||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="SqlDialectMappings">
|
<component name="SqlDialectMappings">
|
||||||
<file url="file://$PROJECT_DIR$/migrations/20200716160921_init.sql" dialect="GenericSQL" />
|
<file url="file://$PROJECT_DIR$/migrations/20200716160921_init.sql" dialect="GenericSQL" />
|
||||||
|
<file url="file://$PROJECT_DIR$/migrations/20200812183213_unique-loaders.sql" dialect="GenericSQL" />
|
||||||
|
<file url="file://$PROJECT_DIR$/migrations/20200928033759_edit-states.sql" dialect="PostgreSQL" />
|
||||||
|
<file url="PROJECT" dialect="PostgreSQL" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
4
migrations/20200928020509_states.sql
Normal file
4
migrations/20200928020509_states.sql
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
CREATE TABLE states (
|
||||||
|
id bigint PRIMARY KEY,
|
||||||
|
url varchar(500)
|
||||||
|
);
|
||||||
4
migrations/20200928033759_edit-states.sql
Normal file
4
migrations/20200928033759_edit-states.sql
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
ALTER TABLE states
|
||||||
|
|
||||||
|
ADD COLUMN expires timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP + interval '1 hour';
|
||||||
3
migrations/20200928053955_make-url-not-null.sql
Normal file
3
migrations/20200928053955_make-url-not-null.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
ALTER TABLE states
|
||||||
|
ALTER COLUMN url SET NOT NULL;
|
||||||
@@ -73,6 +73,13 @@ generate_ids!(
|
|||||||
"SELECT EXISTS(SELECT 1 FROM team_members WHERE id=$1)",
|
"SELECT EXISTS(SELECT 1 FROM team_members WHERE id=$1)",
|
||||||
TeamMemberId
|
TeamMemberId
|
||||||
);
|
);
|
||||||
|
generate_ids!(
|
||||||
|
pub generate_state_id,
|
||||||
|
StateId,
|
||||||
|
8,
|
||||||
|
"SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1)",
|
||||||
|
StateId
|
||||||
|
);
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Type)]
|
#[derive(Copy, Clone, Debug, Type)]
|
||||||
#[sqlx(transparent)]
|
#[sqlx(transparent)]
|
||||||
@@ -109,6 +116,10 @@ pub struct CategoryId(pub i32);
|
|||||||
#[sqlx(transparent)]
|
#[sqlx(transparent)]
|
||||||
pub struct FileId(pub i64);
|
pub struct FileId(pub i64);
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Type)]
|
||||||
|
#[sqlx(transparent)]
|
||||||
|
pub struct StateId(pub i64);
|
||||||
|
|
||||||
use crate::models::ids;
|
use crate::models::ids;
|
||||||
|
|
||||||
impl From<ids::ModId> for ModId {
|
impl From<ids::ModId> for ModId {
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ pub mod base62_impl {
|
|||||||
output
|
output
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_base62(string: &str) -> Result<u64, DecodingError> {
|
pub fn parse_base62(string: &str) -> Result<u64, DecodingError> {
|
||||||
let mut num: u64 = 0;
|
let mut num: u64 = 0;
|
||||||
for c in string.chars() {
|
for c in string.chars() {
|
||||||
let next_digit;
|
let next_digit;
|
||||||
|
|||||||
@@ -1,13 +1,23 @@
|
|||||||
use crate::models::error::ApiError;
|
use crate::models::error::ApiError;
|
||||||
use log::{info};
|
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::{get, HttpResponse};
|
||||||
use actix_web::http::StatusCode;
|
use actix_web::http::StatusCode;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
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) {
|
pub fn config(cfg: &mut ServiceConfig) {
|
||||||
cfg.service(auth_callback);
|
cfg.service(
|
||||||
|
scope("/auth/")
|
||||||
|
.service(auth_callback)
|
||||||
|
.service(init)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
@@ -22,8 +32,11 @@ pub enum AuthorizationError {
|
|||||||
SerDeError(#[from] serde_json::Error),
|
SerDeError(#[from] serde_json::Error),
|
||||||
#[error("Error while communicating to GitHub OAuth2")]
|
#[error("Error while communicating to GitHub OAuth2")]
|
||||||
GithubError(#[from] reqwest::Error),
|
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 {
|
impl actix_web::ResponseError for AuthorizationError {
|
||||||
fn status_code(&self) -> StatusCode {
|
fn status_code(&self) -> StatusCode {
|
||||||
match self {
|
match self {
|
||||||
@@ -32,6 +45,8 @@ impl actix_web::ResponseError for AuthorizationError {
|
|||||||
AuthorizationError::DatabaseError(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
AuthorizationError::DatabaseError(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
AuthorizationError::SerDeError(..) => StatusCode::BAD_REQUEST,
|
AuthorizationError::SerDeError(..) => StatusCode::BAD_REQUEST,
|
||||||
AuthorizationError::GithubError(..) => StatusCode::FAILED_DEPENDENCY,
|
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::DatabaseError(..) => "database_error",
|
||||||
AuthorizationError::SerDeError(..) => "invalid_input",
|
AuthorizationError::SerDeError(..) => "invalid_input",
|
||||||
AuthorizationError::GithubError(..) => "github_error",
|
AuthorizationError::GithubError(..) => "github_error",
|
||||||
|
AuthorizationError::InvalidCredentialsError => "invalid_credentials",
|
||||||
|
AuthorizationError::DecodingError(..) => "decoding_error",
|
||||||
},
|
},
|
||||||
description: &self.to_string(),
|
description: &self.to_string(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct AuthorizationInit {
|
||||||
|
pub url: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct Authorization {
|
pub struct Authorization {
|
||||||
pub code: String,
|
pub code: String,
|
||||||
pub state: Option<String>,
|
pub state: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
@@ -62,8 +84,80 @@ pub struct AccessToken {
|
|||||||
pub token_type: String,
|
pub token_type: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("authcallback")]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub async fn auth_callback(Query(info): Query<Authorization>) -> Result<HttpResponse, AuthorizationError> {
|
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<AuthorizationInit>, client: Data<PgPool>) -> Result<HttpResponse, AuthorizationError> {
|
||||||
|
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<Authorization>, client: Data<PgPool>) -> Result<HttpResponse, AuthorizationError> {
|
||||||
|
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_id = dotenv::var("GITHUB_CLIENT_ID")?;
|
||||||
let client_secret = dotenv::var("GITHUB_CLIENT_SECRET")?;
|
let client_secret = dotenv::var("GITHUB_CLIENT_SECRET")?;
|
||||||
|
|
||||||
@@ -72,7 +166,9 @@ pub async fn auth_callback(Query(info): Query<Authorization>) -> Result<HttpResp
|
|||||||
client_id, client_secret, info.code
|
client_id, client_secret, info.code
|
||||||
);
|
);
|
||||||
|
|
||||||
let token : AccessToken = reqwest::Client::new()
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let token : AccessToken = client
|
||||||
.post(&url)
|
.post(&url)
|
||||||
.header(reqwest::header::ACCEPT, "application/json")
|
.header(reqwest::header::ACCEPT, "application/json")
|
||||||
.send()
|
.send()
|
||||||
@@ -80,5 +176,22 @@ pub async fn auth_callback(Query(info): Query<Authorization>) -> Result<HttpResp
|
|||||||
.json()
|
.json()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().json(token))
|
let user : Value = client
|
||||||
|
.get("https://api.github.com/user")
|
||||||
|
.header(reqwest::header::USER_AGENT, "Modrinth")
|
||||||
|
.header(reqwest::header::AUTHORIZATION, format!("token {}", token.access_token))
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.json()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
transaction.commit().await?;
|
||||||
|
|
||||||
|
let redirect_url = format!("{}?url={}", result.url, token.access_token);
|
||||||
|
|
||||||
|
Ok(HttpResponse::PermanentRedirect()
|
||||||
|
.header("Location", &*redirect_url)
|
||||||
|
.json(AuthorizationInit {
|
||||||
|
url: redirect_url,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user