diff --git a/migrations/20200928170310_create-users.sql b/migrations/20200928170310_create-users.sql new file mode 100644 index 000000000..50fb1c34f --- /dev/null +++ b/migrations/20200928170310_create-users.sql @@ -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 \ No newline at end of file diff --git a/src/database/models/ids.rs b/src/database/models/ids.rs index 4265fb3ec..39e7ee025 100644 --- a/src/database/models/ids.rs +++ b/src/database/models/ids.rs @@ -77,9 +77,16 @@ generate_ids!( pub generate_state_id, StateId, 8, - "SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1)", + "SELECT EXISTS(SELECT 1 FROM states WHERE id=$1)", StateId ); +generate_ids!( + pub generate_user_id, + UserId, + 8, + "SELECT EXISTS(SELECT 1 FROM users WHERE id=$1)", + UserId +); #[derive(Copy, Clone, Debug, Type)] #[sqlx(transparent)] diff --git a/src/database/models/mod.rs b/src/database/models/mod.rs index 67e8e618f..086c3b456 100644 --- a/src/database/models/mod.rs +++ b/src/database/models/mod.rs @@ -8,6 +8,7 @@ pub mod ids; pub mod mod_item; pub mod team_item; pub mod version_item; +pub mod user_item; pub use ids::*; pub use mod_item::Mod; @@ -16,6 +17,7 @@ pub use team_item::TeamMember; pub use version_item::FileHash; pub use version_item::Version; pub use version_item::VersionFile; +pub use user_item::User; #[derive(Error, Debug)] pub enum DatabaseError { diff --git a/src/database/models/user_item.rs b/src/database/models/user_item.rs new file mode 100644 index 000000000..c676be85a --- /dev/null +++ b/src/database/models/user_item.rs @@ -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, + pub avatar_url: String, + pub bio: String, + pub created: chrono::DateTime, +} + +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, 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, 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) + } + } +} \ No newline at end of file diff --git a/src/models/ids.rs b/src/models/ids.rs index 46d287b39..1b98507bd 100644 --- a/src/models/ids.rs +++ b/src/models/ids.rs @@ -1,7 +1,8 @@ use thiserror::Error; 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 /// long when encoded as base62. diff --git a/src/models/mod.rs b/src/models/mod.rs index 603d9fc41..cf01da93f 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -2,3 +2,4 @@ pub mod error; pub mod ids; pub mod mods; pub mod teams; +pub mod users; diff --git a/src/models/teams.rs b/src/models/teams.rs index 80dcb83f4..bc7ddc9da 100644 --- a/src/models/teams.rs +++ b/src/models/teams.rs @@ -1,13 +1,8 @@ use super::ids::Base62Id; use serde::{Deserialize, Serialize}; +use crate::models::users::UserId; //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 #[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(from = "Base62Id")] diff --git a/src/models/users.rs b/src/models/users.rs new file mode 100644 index 000000000..8337d6d7a --- /dev/null +++ b/src/models/users.rs @@ -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, + pub avatar_url: String, + pub bio: String, + pub created: chrono::DateTime, +} \ No newline at end of file diff --git a/src/routes/auth.rs b/src/routes/auth.rs index 93906f5e8..4acea01bd 100644 --- a/src/routes/auth.rs +++ b/src/routes/auth.rs @@ -5,8 +5,7 @@ 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 crate::database::models::{generate_state_id, User, UserId}; use sqlx::postgres::PgPool; use crate::models::ids::base62_impl::{to_base62, parse_base62}; use chrono::Utc; @@ -30,7 +29,7 @@ pub enum AuthorizationError { DatabaseError(#[from] crate::database::models::DatabaseError), #[error("Error while parsing JSON: {0}")] SerDeError(#[from] serde_json::Error), - #[error("Error while communicating to GitHub OAuth2")] + #[error("Error while communicating to GitHub OAuth2: {0}")] GithubError(#[from] reqwest::Error), #[error("Invalid Authentication credentials")] InvalidCredentialsError, @@ -84,14 +83,13 @@ pub struct AccessToken { pub token_type: String, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] pub struct GitHubUser { pub login: String, - pub id: usize, - pub node_id: String, + pub id: u64, pub avatar_url: String, - pub gravatar_id: String, - pub url: String, + pub name: String, + pub email: Option, pub bio: String, } @@ -118,7 +116,7 @@ pub async fn init(Query(info): Query, client: Data) - 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() + Ok(HttpResponse::TemporaryRedirect() .header("Location", &*url) .json(AuthorizationInit { url, @@ -143,7 +141,6 @@ pub async fn auth_callback(Query(info): Query, client: Data, client: Data, client: Data { + 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?; - 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) .json(AuthorizationInit { url: redirect_url, diff --git a/src/search/mod.rs b/src/search/mod.rs index 47ae74ed6..570dd254a 100644 --- a/src/search/mod.rs +++ b/src/search/mod.rs @@ -14,7 +14,7 @@ pub mod indexing; #[derive(Error, Debug)] 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), #[error("Error while serializing or deserializing JSON: {0}")] SerDeError(#[from] serde_json::Error),