You've already forked AstralRinth
forked from didirus/AstralRinth
Implement users in API
This commit is contained in:
8
migrations/20200928170310_create-users.sql
Normal file
8
migrations/20200928170310_create-users.sql
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN github_id bigint NOT NULL default 0,
|
||||||
|
ADD COLUMN username varchar(255) NOT NULL default 'username',
|
||||||
|
ADD COLUMN name varchar(255) NOT NULL default 'John Doe',
|
||||||
|
ADD COLUMN email varchar(255) NULL default 'johndoe@modrinth.com',
|
||||||
|
ADD COLUMN avatar_url varchar(500) NOT NULL default '...',
|
||||||
|
ADD COLUMN bio varchar(160) NOT NULL default 'I make mods!',
|
||||||
|
ADD COLUMN created timestamptz default CURRENT_TIMESTAMP NOT NULL
|
||||||
@@ -77,9 +77,16 @@ generate_ids!(
|
|||||||
pub generate_state_id,
|
pub generate_state_id,
|
||||||
StateId,
|
StateId,
|
||||||
8,
|
8,
|
||||||
"SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1)",
|
"SELECT EXISTS(SELECT 1 FROM states WHERE id=$1)",
|
||||||
StateId
|
StateId
|
||||||
);
|
);
|
||||||
|
generate_ids!(
|
||||||
|
pub generate_user_id,
|
||||||
|
UserId,
|
||||||
|
8,
|
||||||
|
"SELECT EXISTS(SELECT 1 FROM users WHERE id=$1)",
|
||||||
|
UserId
|
||||||
|
);
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Type)]
|
#[derive(Copy, Clone, Debug, Type)]
|
||||||
#[sqlx(transparent)]
|
#[sqlx(transparent)]
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ pub mod ids;
|
|||||||
pub mod mod_item;
|
pub mod mod_item;
|
||||||
pub mod team_item;
|
pub mod team_item;
|
||||||
pub mod version_item;
|
pub mod version_item;
|
||||||
|
pub mod user_item;
|
||||||
|
|
||||||
pub use ids::*;
|
pub use ids::*;
|
||||||
pub use mod_item::Mod;
|
pub use mod_item::Mod;
|
||||||
@@ -16,6 +17,7 @@ pub use team_item::TeamMember;
|
|||||||
pub use version_item::FileHash;
|
pub use version_item::FileHash;
|
||||||
pub use version_item::Version;
|
pub use version_item::Version;
|
||||||
pub use version_item::VersionFile;
|
pub use version_item::VersionFile;
|
||||||
|
pub use user_item::User;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum DatabaseError {
|
pub enum DatabaseError {
|
||||||
|
|||||||
115
src/database/models/user_item.rs
Normal file
115
src/database/models/user_item.rs
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
use super::ids::UserId;
|
||||||
|
|
||||||
|
pub struct User {
|
||||||
|
pub id: UserId,
|
||||||
|
pub github_id: UserId,
|
||||||
|
pub username: String,
|
||||||
|
pub name: String,
|
||||||
|
pub email: Option<String>,
|
||||||
|
pub avatar_url: String,
|
||||||
|
pub bio: String,
|
||||||
|
pub created: chrono::DateTime<chrono::Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl User {
|
||||||
|
pub async fn insert(
|
||||||
|
&self,
|
||||||
|
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||||
|
) -> Result<(), sqlx::error::Error> {
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
INSERT INTO users (
|
||||||
|
id, github_id, username, name, email,
|
||||||
|
avatar_url, bio, created
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
$1, $2, $3, $4, $5,
|
||||||
|
$6, $7, $8
|
||||||
|
)
|
||||||
|
",
|
||||||
|
self.id as UserId,
|
||||||
|
self.github_id as UserId,
|
||||||
|
&self.username,
|
||||||
|
&self.name,
|
||||||
|
self.email.as_ref(),
|
||||||
|
&self.avatar_url,
|
||||||
|
&self.bio,
|
||||||
|
self.created,
|
||||||
|
)
|
||||||
|
.execute(&mut *transaction)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub async fn get<'a, 'b, E>(
|
||||||
|
id: UserId,
|
||||||
|
executor: E,
|
||||||
|
) -> Result<Option<Self>, sqlx::error::Error>
|
||||||
|
where
|
||||||
|
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||||
|
{
|
||||||
|
let result = sqlx::query!(
|
||||||
|
"
|
||||||
|
SELECT u.github_id, u.name, u.email,
|
||||||
|
u.avatar_url, u.username, u.bio,
|
||||||
|
u.created
|
||||||
|
FROM users u
|
||||||
|
WHERE u.id = $1
|
||||||
|
",
|
||||||
|
id as UserId,
|
||||||
|
)
|
||||||
|
.fetch_optional(executor)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if let Some(row) = result {
|
||||||
|
Ok(Some(User {
|
||||||
|
id,
|
||||||
|
github_id: UserId(row.github_id),
|
||||||
|
name: row.name,
|
||||||
|
email: row.email,
|
||||||
|
avatar_url: row.avatar_url,
|
||||||
|
username: row.username,
|
||||||
|
bio: row.bio,
|
||||||
|
created: row.created,
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_from_github_id<'a, 'b, E>(
|
||||||
|
github_id: UserId,
|
||||||
|
executor: E,
|
||||||
|
) -> Result<Option<Self>, sqlx::error::Error>
|
||||||
|
where
|
||||||
|
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||||
|
{
|
||||||
|
let result = sqlx::query!(
|
||||||
|
"
|
||||||
|
SELECT u.id, u.name, u.email,
|
||||||
|
u.avatar_url, u.username, u.bio,
|
||||||
|
u.created
|
||||||
|
FROM users u
|
||||||
|
WHERE u.github_id = $1
|
||||||
|
",
|
||||||
|
github_id as UserId,
|
||||||
|
)
|
||||||
|
.fetch_optional(executor)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if let Some(row) = result {
|
||||||
|
Ok(Some(User {
|
||||||
|
id: UserId(row.id),
|
||||||
|
github_id,
|
||||||
|
name: row.name,
|
||||||
|
email: row.email,
|
||||||
|
avatar_url: row.avatar_url,
|
||||||
|
username: row.username,
|
||||||
|
bio: row.bio,
|
||||||
|
created: row.created,
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
pub use super::mods::{ModId, VersionId};
|
pub use super::mods::{ModId, VersionId};
|
||||||
pub use super::teams::{TeamId, UserId};
|
pub use super::teams::TeamId;
|
||||||
|
pub use super::users::UserId;
|
||||||
|
|
||||||
/// Generates a random 64 bit integer that is exactly `n` characters
|
/// Generates a random 64 bit integer that is exactly `n` characters
|
||||||
/// long when encoded as base62.
|
/// long when encoded as base62.
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ pub mod error;
|
|||||||
pub mod ids;
|
pub mod ids;
|
||||||
pub mod mods;
|
pub mod mods;
|
||||||
pub mod teams;
|
pub mod teams;
|
||||||
|
pub mod users;
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
use super::ids::Base62Id;
|
use super::ids::Base62Id;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use crate::models::users::UserId;
|
||||||
|
|
||||||
//TODO Implement Item for teams
|
//TODO Implement Item for teams
|
||||||
/// The ID of a specific user, encoded as base62 for usage in the API
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
#[serde(from = "Base62Id")]
|
|
||||||
#[serde(into = "Base62Id")]
|
|
||||||
pub struct UserId(pub u64);
|
|
||||||
|
|
||||||
/// The ID of a team
|
/// The ID of a team
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(from = "Base62Id")]
|
#[serde(from = "Base62Id")]
|
||||||
|
|||||||
19
src/models/users.rs
Normal file
19
src/models/users.rs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
use super::ids::Base62Id;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(from = "Base62Id")]
|
||||||
|
#[serde(into = "Base62Id")]
|
||||||
|
pub struct UserId(pub u64);
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct User {
|
||||||
|
pub id: UserId,
|
||||||
|
pub github_id: UserId,
|
||||||
|
pub username: String,
|
||||||
|
pub name: String,
|
||||||
|
pub email: Option<String>,
|
||||||
|
pub avatar_url: String,
|
||||||
|
pub bio: String,
|
||||||
|
pub created: chrono::DateTime<chrono::Utc>,
|
||||||
|
}
|
||||||
@@ -5,8 +5,7 @@ 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, User, UserId};
|
||||||
use crate::database::models::generate_state_id;
|
|
||||||
use sqlx::postgres::PgPool;
|
use sqlx::postgres::PgPool;
|
||||||
use crate::models::ids::base62_impl::{to_base62, parse_base62};
|
use crate::models::ids::base62_impl::{to_base62, parse_base62};
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
@@ -30,7 +29,7 @@ pub enum AuthorizationError {
|
|||||||
DatabaseError(#[from] crate::database::models::DatabaseError),
|
DatabaseError(#[from] crate::database::models::DatabaseError),
|
||||||
#[error("Error while parsing JSON: {0}")]
|
#[error("Error while parsing JSON: {0}")]
|
||||||
SerDeError(#[from] serde_json::Error),
|
SerDeError(#[from] serde_json::Error),
|
||||||
#[error("Error while communicating to GitHub OAuth2")]
|
#[error("Error while communicating to GitHub OAuth2: {0}")]
|
||||||
GithubError(#[from] reqwest::Error),
|
GithubError(#[from] reqwest::Error),
|
||||||
#[error("Invalid Authentication credentials")]
|
#[error("Invalid Authentication credentials")]
|
||||||
InvalidCredentialsError,
|
InvalidCredentialsError,
|
||||||
@@ -84,14 +83,13 @@ pub struct AccessToken {
|
|||||||
pub token_type: String,
|
pub token_type: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct GitHubUser {
|
pub struct GitHubUser {
|
||||||
pub login: String,
|
pub login: String,
|
||||||
pub id: usize,
|
pub id: u64,
|
||||||
pub node_id: String,
|
|
||||||
pub avatar_url: String,
|
pub avatar_url: String,
|
||||||
pub gravatar_id: String,
|
pub name: String,
|
||||||
pub url: String,
|
pub email: Option<String>,
|
||||||
pub bio: String,
|
pub bio: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +116,7 @@ pub async fn init(Query(info): Query<AuthorizationInit>, client: Data<PgPool>) -
|
|||||||
let client_id = dotenv::var("GITHUB_CLIENT_ID")?;
|
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");
|
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()
|
Ok(HttpResponse::TemporaryRedirect()
|
||||||
.header("Location", &*url)
|
.header("Location", &*url)
|
||||||
.json(AuthorizationInit {
|
.json(AuthorizationInit {
|
||||||
url,
|
url,
|
||||||
@@ -143,7 +141,6 @@ pub async fn auth_callback(Query(info): Query<Authorization>, client: Data<PgPoo
|
|||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
let duration = result.expires.signed_duration_since(now);
|
let duration = result.expires.signed_duration_since(now);
|
||||||
|
|
||||||
info!("{:?}", duration.num_seconds());
|
|
||||||
if duration.num_seconds() < 0 {
|
if duration.num_seconds() < 0 {
|
||||||
return Err(AuthorizationError::InvalidCredentialsError);
|
return Err(AuthorizationError::InvalidCredentialsError);
|
||||||
}
|
}
|
||||||
@@ -176,7 +173,7 @@ pub async fn auth_callback(Query(info): Query<Authorization>, client: Data<PgPoo
|
|||||||
.json()
|
.json()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let user : Value = client
|
let user : GitHubUser = client
|
||||||
.get("https://api.github.com/user")
|
.get("https://api.github.com/user")
|
||||||
.header(reqwest::header::USER_AGENT, "Modrinth")
|
.header(reqwest::header::USER_AGENT, "Modrinth")
|
||||||
.header(reqwest::header::AUTHORIZATION, format!("token {}", token.access_token))
|
.header(reqwest::header::AUTHORIZATION, format!("token {}", token.access_token))
|
||||||
@@ -185,11 +182,32 @@ pub async fn auth_callback(Query(info): Query<Authorization>, client: Data<PgPoo
|
|||||||
.json()
|
.json()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let user_result = User::get_from_github_id(UserId(user.id as i64), &mut *transaction).await?;
|
||||||
|
match user_result{
|
||||||
|
Some(x) => {
|
||||||
|
info!("{:?}", x.id)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let user_id = crate::database::models::generate_user_id(&mut transaction).await?.into();
|
||||||
|
|
||||||
|
User {
|
||||||
|
id: user_id,
|
||||||
|
github_id: UserId(user.id as i64),
|
||||||
|
username: user.login,
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
avatar_url: user.avatar_url,
|
||||||
|
bio: user.bio,
|
||||||
|
created: Utc::now()
|
||||||
|
}.insert(&mut transaction).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
transaction.commit().await?;
|
transaction.commit().await?;
|
||||||
|
|
||||||
let redirect_url = format!("{}?url={}", result.url, token.access_token);
|
let redirect_url = format!("{}?code={}", result.url, token.access_token);
|
||||||
|
|
||||||
Ok(HttpResponse::PermanentRedirect()
|
Ok(HttpResponse::TemporaryRedirect()
|
||||||
.header("Location", &*redirect_url)
|
.header("Location", &*redirect_url)
|
||||||
.json(AuthorizationInit {
|
.json(AuthorizationInit {
|
||||||
url: redirect_url,
|
url: redirect_url,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ pub mod indexing;
|
|||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum SearchError {
|
pub enum SearchError {
|
||||||
#[error("Error while connecting to the MeiliSearch database")]
|
#[error("Error while connecting to the MeiliSearch database: {0}")]
|
||||||
IndexDBError(#[from] meilisearch_sdk::errors::Error),
|
IndexDBError(#[from] meilisearch_sdk::errors::Error),
|
||||||
#[error("Error while serializing or deserializing JSON: {0}")]
|
#[error("Error while serializing or deserializing JSON: {0}")]
|
||||||
SerDeError(#[from] serde_json::Error),
|
SerDeError(#[from] serde_json::Error),
|
||||||
|
|||||||
Reference in New Issue
Block a user