Add REST APIs for signing the CLA, retrieving users who've signed the CLA (#4202)
This commit is contained in:
Generated
+1
@@ -1463,6 +1463,7 @@ dependencies = [
|
||||
"base64 0.13.1",
|
||||
"call",
|
||||
"channel",
|
||||
"chrono",
|
||||
"clap 3.2.25",
|
||||
"client",
|
||||
"clock",
|
||||
|
||||
@@ -93,6 +93,7 @@ resolver = "2"
|
||||
anyhow = { version = "1.0.57" }
|
||||
async-trait = { version = "0.1" }
|
||||
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
ctor = "0.2.6"
|
||||
derive_more = { version = "0.99.17" }
|
||||
env_logger = { version = "0.9" }
|
||||
|
||||
@@ -30,7 +30,7 @@ workspace = { path = "../workspace" }
|
||||
uuid.workspace = true
|
||||
log.workspace = true
|
||||
anyhow.workspace = true
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
chrono.workspace = true
|
||||
futures.workspace = true
|
||||
indoc.workspace = true
|
||||
isahc.workspace = true
|
||||
|
||||
@@ -27,6 +27,7 @@ axum = { version = "0.5", features = ["json", "headers", "ws"] }
|
||||
axum-extra = { version = "0.3", features = ["erased-json"] }
|
||||
base64 = "0.13"
|
||||
clap = { version = "3.1", features = ["derive"], optional = true }
|
||||
chrono.workspace = true
|
||||
dashmap = "5.4"
|
||||
envy = "0.4.2"
|
||||
futures.workspace = true
|
||||
|
||||
@@ -344,3 +344,9 @@ CREATE INDEX
|
||||
"index_notifications_on_recipient_id_is_read_kind_entity_id"
|
||||
ON "notifications"
|
||||
("recipient_id", "is_read", "kind", "entity_id");
|
||||
|
||||
CREATE TABLE contributors (
|
||||
user_id INTEGER REFERENCES users(id),
|
||||
signed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (user_id)
|
||||
);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
CREATE TABLE contributors (
|
||||
user_id INTEGER REFERENCES users(id),
|
||||
signed_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (user_id)
|
||||
);
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
auth,
|
||||
db::{User, UserId},
|
||||
db::{ContributorSelector, User, UserId},
|
||||
rpc, AppState, Error, Result,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
@@ -14,6 +14,7 @@ use axum::{
|
||||
Extension, Json, Router,
|
||||
};
|
||||
use axum_extra::response::ErasedJson;
|
||||
use chrono::SecondsFormat;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tower::ServiceBuilder;
|
||||
@@ -25,6 +26,8 @@ pub fn routes(rpc_server: Arc<rpc::Server>, state: Arc<AppState>) -> Router<Body
|
||||
.route("/users/:id/access_tokens", post(create_access_token))
|
||||
.route("/panic", post(trace_panic))
|
||||
.route("/rpc_server_snapshot", get(get_rpc_server_snapshot))
|
||||
.route("/contributors", get(get_contributors).post(add_contributor))
|
||||
.route("/contributor", get(check_is_contributor))
|
||||
.layer(
|
||||
ServiceBuilder::new()
|
||||
.layer(Extension(state))
|
||||
@@ -66,7 +69,7 @@ pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoR
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AuthenticatedUserParams {
|
||||
github_user_id: Option<i32>,
|
||||
github_user_id: i32,
|
||||
github_login: String,
|
||||
github_email: Option<String>,
|
||||
}
|
||||
@@ -88,8 +91,7 @@ async fn get_authenticated_user(
|
||||
params.github_user_id,
|
||||
params.github_email.as_deref(),
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "user not found".into()))?;
|
||||
.await?;
|
||||
let metrics_id = app.db.get_user_metrics_id(user.id).await?;
|
||||
return Ok(Json(AuthenticatedUserResponse { user, metrics_id }));
|
||||
}
|
||||
@@ -133,6 +135,65 @@ async fn get_rpc_server_snapshot(
|
||||
Ok(ErasedJson::pretty(rpc_server.snapshot().await))
|
||||
}
|
||||
|
||||
async fn get_contributors(Extension(app): Extension<Arc<AppState>>) -> Result<Json<Vec<String>>> {
|
||||
Ok(Json(app.db.get_contributors().await?))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CheckIsContributorParams {
|
||||
github_user_id: Option<i32>,
|
||||
github_login: Option<String>,
|
||||
}
|
||||
|
||||
impl CheckIsContributorParams {
|
||||
fn as_contributor_selector(self) -> Result<ContributorSelector> {
|
||||
if let Some(github_user_id) = self.github_user_id {
|
||||
return Ok(ContributorSelector::GitHubUserId { github_user_id });
|
||||
}
|
||||
|
||||
if let Some(github_login) = self.github_login {
|
||||
return Ok(ContributorSelector::GitHubLogin { github_login });
|
||||
}
|
||||
|
||||
Err(anyhow!(
|
||||
"must be one of `github_user_id` or `github_login`."
|
||||
))?
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct CheckIsContributorResponse {
|
||||
signed_at: Option<String>,
|
||||
}
|
||||
|
||||
async fn check_is_contributor(
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
Query(params): Query<CheckIsContributorParams>,
|
||||
) -> Result<Json<CheckIsContributorResponse>> {
|
||||
let params = params.as_contributor_selector()?;
|
||||
Ok(Json(CheckIsContributorResponse {
|
||||
signed_at: app
|
||||
.db
|
||||
.get_contributor_sign_timestamp(¶ms)
|
||||
.await?
|
||||
.map(|ts| ts.and_utc().to_rfc3339_opts(SecondsFormat::Millis, true)),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn add_contributor(
|
||||
Json(params): Json<AuthenticatedUserParams>,
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
) -> Result<()> {
|
||||
Ok(app
|
||||
.db
|
||||
.add_contributor(
|
||||
¶ms.github_login,
|
||||
params.github_user_id,
|
||||
params.github_email.as_deref(),
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CreateAccessTokenQueryParams {
|
||||
public_key: String,
|
||||
|
||||
@@ -68,7 +68,7 @@ async fn main() {
|
||||
user_count += 1;
|
||||
db.get_or_create_user_by_github_account(
|
||||
&github_user.login,
|
||||
Some(github_user.id),
|
||||
github_user.id,
|
||||
github_user.email.as_deref(),
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -44,6 +44,7 @@ use tables::*;
|
||||
use tokio::sync::{Mutex, OwnedMutexGuard};
|
||||
|
||||
pub use ids::*;
|
||||
pub use queries::contributors::ContributorSelector;
|
||||
pub use sea_orm::ConnectOptions;
|
||||
pub use tables::user::Model as User;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ pub mod access_tokens;
|
||||
pub mod buffers;
|
||||
pub mod channels;
|
||||
pub mod contacts;
|
||||
pub mod contributors;
|
||||
pub mod messages;
|
||||
pub mod notifications;
|
||||
pub mod projects;
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ContributorSelector {
|
||||
GitHubUserId { github_user_id: i32 },
|
||||
GitHubLogin { github_login: String },
|
||||
}
|
||||
|
||||
impl Database {
|
||||
/// Retrieves the GitHub logins of all users who have signed the CLA.
|
||||
pub async fn get_contributors(&self) -> Result<Vec<String>> {
|
||||
self.transaction(|tx| async move {
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||
enum QueryGithubLogin {
|
||||
GithubLogin,
|
||||
}
|
||||
|
||||
Ok(contributor::Entity::find()
|
||||
.inner_join(user::Entity)
|
||||
.order_by_asc(contributor::Column::SignedAt)
|
||||
.select_only()
|
||||
.column(user::Column::GithubLogin)
|
||||
.into_values::<_, QueryGithubLogin>()
|
||||
.all(&*tx)
|
||||
.await?)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Records that a given user has signed the CLA.
|
||||
pub async fn get_contributor_sign_timestamp(
|
||||
&self,
|
||||
selector: &ContributorSelector,
|
||||
) -> Result<Option<DateTime>> {
|
||||
self.transaction(|tx| async move {
|
||||
let condition = match selector {
|
||||
ContributorSelector::GitHubUserId { github_user_id } => {
|
||||
user::Column::GithubUserId.eq(*github_user_id)
|
||||
}
|
||||
ContributorSelector::GitHubLogin { github_login } => {
|
||||
user::Column::GithubLogin.eq(github_login)
|
||||
}
|
||||
};
|
||||
|
||||
let Some(user) = user::Entity::find().filter(condition).one(&*tx).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(contributor) = contributor::Entity::find_by_id(user.id).one(&*tx).await?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
Ok(Some(contributor.signed_at))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Records that a given user has signed the CLA.
|
||||
pub async fn add_contributor(
|
||||
&self,
|
||||
github_login: &str,
|
||||
github_user_id: i32,
|
||||
github_email: Option<&str>,
|
||||
) -> Result<()> {
|
||||
self.transaction(|tx| async move {
|
||||
let user = self
|
||||
.get_or_create_user_by_github_account_tx(
|
||||
github_login,
|
||||
github_user_id,
|
||||
github_email,
|
||||
&*tx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
contributor::Entity::insert(contributor::ActiveModel {
|
||||
user_id: ActiveValue::Set(user.id),
|
||||
signed_at: ActiveValue::NotSet,
|
||||
})
|
||||
.on_conflict(
|
||||
OnConflict::column(contributor::Column::UserId)
|
||||
.do_nothing()
|
||||
.to_owned(),
|
||||
)
|
||||
.exec_without_returning(&*tx)
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -72,53 +72,61 @@ impl Database {
|
||||
pub async fn get_or_create_user_by_github_account(
|
||||
&self,
|
||||
github_login: &str,
|
||||
github_user_id: Option<i32>,
|
||||
github_user_id: i32,
|
||||
github_email: Option<&str>,
|
||||
) -> Result<Option<User>> {
|
||||
) -> Result<User> {
|
||||
self.transaction(|tx| async move {
|
||||
let tx = &*tx;
|
||||
if let Some(github_user_id) = github_user_id {
|
||||
if let Some(user_by_github_user_id) = user::Entity::find()
|
||||
.filter(user::Column::GithubUserId.eq(github_user_id))
|
||||
.one(tx)
|
||||
.await?
|
||||
{
|
||||
let mut user_by_github_user_id = user_by_github_user_id.into_active_model();
|
||||
user_by_github_user_id.github_login = ActiveValue::set(github_login.into());
|
||||
Ok(Some(user_by_github_user_id.update(tx).await?))
|
||||
} else if let Some(user_by_github_login) = user::Entity::find()
|
||||
.filter(user::Column::GithubLogin.eq(github_login))
|
||||
.one(tx)
|
||||
.await?
|
||||
{
|
||||
let mut user_by_github_login = user_by_github_login.into_active_model();
|
||||
user_by_github_login.github_user_id = ActiveValue::set(Some(github_user_id));
|
||||
Ok(Some(user_by_github_login.update(tx).await?))
|
||||
} else {
|
||||
let user = user::Entity::insert(user::ActiveModel {
|
||||
email_address: ActiveValue::set(github_email.map(|email| email.into())),
|
||||
github_login: ActiveValue::set(github_login.into()),
|
||||
github_user_id: ActiveValue::set(Some(github_user_id)),
|
||||
admin: ActiveValue::set(false),
|
||||
invite_count: ActiveValue::set(0),
|
||||
invite_code: ActiveValue::set(None),
|
||||
metrics_id: ActiveValue::set(Uuid::new_v4()),
|
||||
..Default::default()
|
||||
})
|
||||
.exec_with_returning(&*tx)
|
||||
.await?;
|
||||
Ok(Some(user))
|
||||
}
|
||||
} else {
|
||||
Ok(user::Entity::find()
|
||||
.filter(user::Column::GithubLogin.eq(github_login))
|
||||
.one(tx)
|
||||
.await?)
|
||||
}
|
||||
self.get_or_create_user_by_github_account_tx(
|
||||
github_login,
|
||||
github_user_id,
|
||||
github_email,
|
||||
&*tx,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_or_create_user_by_github_account_tx(
|
||||
&self,
|
||||
github_login: &str,
|
||||
github_user_id: i32,
|
||||
github_email: Option<&str>,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<User> {
|
||||
if let Some(user_by_github_user_id) = user::Entity::find()
|
||||
.filter(user::Column::GithubUserId.eq(github_user_id))
|
||||
.one(tx)
|
||||
.await?
|
||||
{
|
||||
let mut user_by_github_user_id = user_by_github_user_id.into_active_model();
|
||||
user_by_github_user_id.github_login = ActiveValue::set(github_login.into());
|
||||
Ok(user_by_github_user_id.update(tx).await?)
|
||||
} else if let Some(user_by_github_login) = user::Entity::find()
|
||||
.filter(user::Column::GithubLogin.eq(github_login))
|
||||
.one(tx)
|
||||
.await?
|
||||
{
|
||||
let mut user_by_github_login = user_by_github_login.into_active_model();
|
||||
user_by_github_login.github_user_id = ActiveValue::set(Some(github_user_id));
|
||||
Ok(user_by_github_login.update(tx).await?)
|
||||
} else {
|
||||
let user = user::Entity::insert(user::ActiveModel {
|
||||
email_address: ActiveValue::set(github_email.map(|email| email.into())),
|
||||
github_login: ActiveValue::set(github_login.into()),
|
||||
github_user_id: ActiveValue::set(Some(github_user_id)),
|
||||
admin: ActiveValue::set(false),
|
||||
invite_count: ActiveValue::set(0),
|
||||
invite_code: ActiveValue::set(None),
|
||||
metrics_id: ActiveValue::set(Uuid::new_v4()),
|
||||
..Default::default()
|
||||
})
|
||||
.exec_with_returning(&*tx)
|
||||
.await?;
|
||||
Ok(user)
|
||||
}
|
||||
}
|
||||
|
||||
/// get_all_users returns the next page of users. To get more call again with
|
||||
/// the same limit and the page incremented by 1.
|
||||
pub async fn get_all_users(&self, page: u32, limit: u32) -> Result<Vec<User>> {
|
||||
|
||||
@@ -9,6 +9,7 @@ pub mod channel_member;
|
||||
pub mod channel_message;
|
||||
pub mod channel_message_mention;
|
||||
pub mod contact;
|
||||
pub mod contributor;
|
||||
pub mod feature_flag;
|
||||
pub mod follower;
|
||||
pub mod language_server;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
use crate::db::UserId;
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::Serialize;
|
||||
|
||||
/// A user who has signed the CLA.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel, Serialize)]
|
||||
#[sea_orm(table_name = "contributors")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub user_id: UserId,
|
||||
pub signed_at: DateTime,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::user::Entity",
|
||||
from = "Column::UserId",
|
||||
to = "super::user::Column::Id"
|
||||
)]
|
||||
User,
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
impl Related<super::user::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::User.def()
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,8 @@ pub enum Relation {
|
||||
ChannelMemberships,
|
||||
#[sea_orm(has_many = "super::user_feature::Entity")]
|
||||
UserFeatures,
|
||||
#[sea_orm(has_one = "super::contributor::Entity")]
|
||||
Contributor,
|
||||
}
|
||||
|
||||
impl Related<super::access_token::Entity> for Entity {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
mod buffer_tests;
|
||||
mod channel_tests;
|
||||
mod contributor_tests;
|
||||
mod db_tests;
|
||||
mod feature_flag_tests;
|
||||
mod message_tests;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
use super::Database;
|
||||
use crate::{db::NewUserParams, test_both_dbs};
|
||||
use std::sync::Arc;
|
||||
|
||||
test_both_dbs!(
|
||||
test_contributors,
|
||||
test_contributors_postgres,
|
||||
test_contributors_sqlite
|
||||
);
|
||||
|
||||
async fn test_contributors(db: &Arc<Database>) {
|
||||
db.create_user(
|
||||
&format!("user1@example.com"),
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: format!("user1"),
|
||||
github_user_id: 1,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
assert_eq!(db.get_contributors().await.unwrap(), Vec::<String>::new());
|
||||
|
||||
db.add_contributor("user1", 1, None).await.unwrap();
|
||||
assert_eq!(
|
||||
db.get_contributors().await.unwrap(),
|
||||
vec!["user1".to_string()]
|
||||
);
|
||||
|
||||
db.add_contributor("user2", 2, None).await.unwrap();
|
||||
assert_eq!(
|
||||
db.get_contributors().await.unwrap(),
|
||||
vec!["user1".to_string(), "user2".to_string()]
|
||||
);
|
||||
}
|
||||
@@ -80,18 +80,17 @@ test_both_dbs!(
|
||||
);
|
||||
|
||||
async fn test_get_or_create_user_by_github_account(db: &Arc<Database>) {
|
||||
let user_id1 = db
|
||||
.create_user(
|
||||
"user1@example.com",
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "login1".into(),
|
||||
github_user_id: 101,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
db.create_user(
|
||||
"user1@example.com",
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "login1".into(),
|
||||
github_user_id: 101,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
let user_id2 = db
|
||||
.create_user(
|
||||
"user2@example.com",
|
||||
@@ -106,33 +105,16 @@ async fn test_get_or_create_user_by_github_account(db: &Arc<Database>) {
|
||||
.user_id;
|
||||
|
||||
let user = db
|
||||
.get_or_create_user_by_github_account("login1", None, None)
|
||||
.get_or_create_user_by_github_account("the-new-login2", 102, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(user.id, user_id1);
|
||||
assert_eq!(&user.github_login, "login1");
|
||||
assert_eq!(user.github_user_id, Some(101));
|
||||
|
||||
assert!(db
|
||||
.get_or_create_user_by_github_account("non-existent-login", None, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none());
|
||||
|
||||
let user = db
|
||||
.get_or_create_user_by_github_account("the-new-login2", Some(102), None)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(user.id, user_id2);
|
||||
assert_eq!(&user.github_login, "the-new-login2");
|
||||
assert_eq!(user.github_user_id, Some(102));
|
||||
|
||||
let user = db
|
||||
.get_or_create_user_by_github_account("login3", Some(103), Some("user3@example.com"))
|
||||
.get_or_create_user_by_github_account("login3", 103, Some("user3@example.com"))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(&user.github_login, "login3");
|
||||
assert_eq!(user.github_user_id, Some(103));
|
||||
|
||||
Reference in New Issue
Block a user