collab: Make users.github_user_id required and unique (#16704)

This PR makes the `github_user_id` column on the `users` table required
and replaces the index with a unique index.

I have gone through and ensured that all users have a unique
`github_user_id` in the staging and production databases.

Release Notes:

- N/A
This commit is contained in:
Marshall Bowers 2024-08-22 18:27:22 -04:00 committed by GitHub
parent 69e76a3bb9
commit 4ddf2cbb9f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 74 additions and 85 deletions

View file

@ -9,14 +9,14 @@ CREATE TABLE "users" (
"connected_once" BOOLEAN NOT NULL DEFAULT false, "connected_once" BOOLEAN NOT NULL DEFAULT false,
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
"metrics_id" TEXT, "metrics_id" TEXT,
"github_user_id" INTEGER, "github_user_id" INTEGER NOT NULL,
"accepted_tos_at" TIMESTAMP WITHOUT TIME ZONE, "accepted_tos_at" TIMESTAMP WITHOUT TIME ZONE,
"github_user_created_at" TIMESTAMP WITHOUT TIME ZONE "github_user_created_at" TIMESTAMP WITHOUT TIME ZONE
); );
CREATE UNIQUE INDEX "index_users_github_login" ON "users" ("github_login"); CREATE UNIQUE INDEX "index_users_github_login" ON "users" ("github_login");
CREATE UNIQUE INDEX "index_invite_code_users" ON "users" ("invite_code"); CREATE UNIQUE INDEX "index_invite_code_users" ON "users" ("invite_code");
CREATE INDEX "index_users_on_email_address" ON "users" ("email_address"); CREATE INDEX "index_users_on_email_address" ON "users" ("email_address");
CREATE INDEX "index_users_on_github_user_id" ON "users" ("github_user_id"); CREATE UNIQUE INDEX "index_users_on_github_user_id" ON "users" ("github_user_id");
CREATE TABLE "access_tokens" ( CREATE TABLE "access_tokens" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT, "id" INTEGER PRIMARY KEY AUTOINCREMENT,

View file

@ -0,0 +1,4 @@
alter table users alter column github_user_id set not null;
drop index index_users_on_github_user_id;
create unique index uix_users_on_github_user_id on users (github_user_id);

View file

@ -108,7 +108,7 @@ pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoR
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct AuthenticatedUserParams { struct AuthenticatedUserParams {
github_user_id: Option<i32>, github_user_id: i32,
github_login: String, github_login: String,
github_email: Option<String>, github_email: Option<String>,
github_user_created_at: Option<chrono::DateTime<chrono::Utc>>, github_user_created_at: Option<chrono::DateTime<chrono::Utc>>,

View file

@ -63,7 +63,7 @@ impl Database {
pub async fn add_contributor( pub async fn add_contributor(
&self, &self,
github_login: &str, github_login: &str,
github_user_id: Option<i32>, github_user_id: i32,
github_email: Option<&str>, github_email: Option<&str>,
github_user_created_at: Option<DateTimeUtc>, github_user_created_at: Option<DateTimeUtc>,
initial_channel_id: Option<ChannelId>, initial_channel_id: Option<ChannelId>,

View file

@ -15,7 +15,7 @@ impl Database {
let user = user::Entity::insert(user::ActiveModel { let user = user::Entity::insert(user::ActiveModel {
email_address: ActiveValue::set(Some(email_address.into())), email_address: ActiveValue::set(Some(email_address.into())),
github_login: ActiveValue::set(params.github_login.clone()), github_login: ActiveValue::set(params.github_login.clone()),
github_user_id: ActiveValue::set(Some(params.github_user_id)), github_user_id: ActiveValue::set(params.github_user_id),
admin: ActiveValue::set(admin), admin: ActiveValue::set(admin),
metrics_id: ActiveValue::set(Uuid::new_v4()), metrics_id: ActiveValue::set(Uuid::new_v4()),
..Default::default() ..Default::default()
@ -99,7 +99,7 @@ impl Database {
pub async fn get_or_create_user_by_github_account( pub async fn get_or_create_user_by_github_account(
&self, &self,
github_login: &str, github_login: &str,
github_user_id: Option<i32>, github_user_id: i32,
github_email: Option<&str>, github_email: Option<&str>,
github_user_created_at: Option<DateTimeUtc>, github_user_created_at: Option<DateTimeUtc>,
initial_channel_id: Option<ChannelId>, initial_channel_id: Option<ChannelId>,
@ -121,70 +121,61 @@ impl Database {
pub async fn get_or_create_user_by_github_account_tx( pub async fn get_or_create_user_by_github_account_tx(
&self, &self,
github_login: &str, github_login: &str,
github_user_id: Option<i32>, github_user_id: i32,
github_email: Option<&str>, github_email: Option<&str>,
github_user_created_at: Option<NaiveDateTime>, github_user_created_at: Option<NaiveDateTime>,
initial_channel_id: Option<ChannelId>, initial_channel_id: Option<ChannelId>,
tx: &DatabaseTransaction, tx: &DatabaseTransaction,
) -> Result<User> { ) -> Result<User> {
if let Some(github_user_id) = github_user_id { if let Some(user_by_github_user_id) = user::Entity::find()
if let Some(user_by_github_user_id) = user::Entity::find() .filter(user::Column::GithubUserId.eq(github_user_id))
.filter(user::Column::GithubUserId.eq(github_user_id)) .one(tx)
.one(tx) .await?
.await? {
{ let mut user_by_github_user_id = user_by_github_user_id.into_active_model();
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());
user_by_github_user_id.github_login = ActiveValue::set(github_login.into()); if github_user_created_at.is_some() {
if github_user_created_at.is_some() { user_by_github_user_id.github_user_created_at =
user_by_github_user_id.github_user_created_at = ActiveValue::set(github_user_created_at);
ActiveValue::set(github_user_created_at);
}
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));
if github_user_created_at.is_some() {
user_by_github_login.github_user_created_at =
ActiveValue::set(github_user_created_at);
}
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)),
github_user_created_at: ActiveValue::set(github_user_created_at),
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?;
if let Some(channel_id) = initial_channel_id {
channel_member::Entity::insert(channel_member::ActiveModel {
id: ActiveValue::NotSet,
channel_id: ActiveValue::Set(channel_id),
user_id: ActiveValue::Set(user.id),
accepted: ActiveValue::Set(true),
role: ActiveValue::Set(ChannelRole::Guest),
})
.exec(tx)
.await?;
}
Ok(user)
} }
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(github_user_id);
if github_user_created_at.is_some() {
user_by_github_login.github_user_created_at =
ActiveValue::set(github_user_created_at);
}
Ok(user_by_github_login.update(tx).await?)
} else { } else {
let user = user::Entity::find() let user = user::Entity::insert(user::ActiveModel {
.filter(user::Column::GithubLogin.eq(github_login)) email_address: ActiveValue::set(github_email.map(|email| email.into())),
.one(tx) github_login: ActiveValue::set(github_login.into()),
.await? github_user_id: ActiveValue::set(github_user_id),
.ok_or_else(|| anyhow!("no such user {}", github_login))?; github_user_created_at: ActiveValue::set(github_user_created_at),
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?;
if let Some(channel_id) = initial_channel_id {
channel_member::Entity::insert(channel_member::ActiveModel {
id: ActiveValue::NotSet,
channel_id: ActiveValue::Set(channel_id),
user_id: ActiveValue::Set(user.id),
accepted: ActiveValue::Set(true),
role: ActiveValue::Set(ChannelRole::Guest),
})
.exec(tx)
.await?;
}
Ok(user) Ok(user)
} }
} }

View file

@ -10,7 +10,7 @@ pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]
pub id: UserId, pub id: UserId,
pub github_login: String, pub github_login: String,
pub github_user_id: Option<i32>, pub github_user_id: i32,
pub github_user_created_at: Option<NaiveDateTime>, pub github_user_created_at: Option<NaiveDateTime>,
pub email_address: Option<String>, pub email_address: Option<String>,
pub admin: bool, pub admin: bool,

View file

@ -42,7 +42,7 @@ async fn test_channel_buffers(db: &Arc<Database>) {
false, false,
NewUserParams { NewUserParams {
github_login: "user_c".into(), github_login: "user_c".into(),
github_user_id: 102, github_user_id: 103,
}, },
) )
.await .await

View file

@ -25,7 +25,7 @@ async fn test_contributors(db: &Arc<Database>) {
assert_eq!(db.get_contributors().await.unwrap(), Vec::<String>::new()); assert_eq!(db.get_contributors().await.unwrap(), Vec::<String>::new());
let user1_created_at = Utc::now(); let user1_created_at = Utc::now();
db.add_contributor("user1", Some(1), None, Some(user1_created_at), None) db.add_contributor("user1", 1, None, Some(user1_created_at), None)
.await .await
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
@ -34,7 +34,7 @@ async fn test_contributors(db: &Arc<Database>) {
); );
let user2_created_at = Utc::now(); let user2_created_at = Utc::now();
db.add_contributor("user2", Some(2), None, Some(user2_created_at), None) db.add_contributor("user2", 2, None, Some(user2_created_at), None)
.await .await
.unwrap(); .unwrap();
assert_eq!( assert_eq!(

View file

@ -45,25 +45,25 @@ async fn test_get_users(db: &Arc<Database>) {
( (
user_ids[0], user_ids[0],
"user1".to_string(), "user1".to_string(),
Some(1), 1,
Some("user1@example.com".to_string()), Some("user1@example.com".to_string()),
), ),
( (
user_ids[1], user_ids[1],
"user2".to_string(), "user2".to_string(),
Some(2), 2,
Some("user2@example.com".to_string()), Some("user2@example.com".to_string()),
), ),
( (
user_ids[2], user_ids[2],
"user3".to_string(), "user3".to_string(),
Some(3), 3,
Some("user3@example.com".to_string()), Some("user3@example.com".to_string()),
), ),
( (
user_ids[3], user_ids[3],
"user4".to_string(), "user4".to_string(),
Some(4), 4,
Some("user4@example.com".to_string()), Some("user4@example.com".to_string()),
) )
] ]
@ -101,23 +101,17 @@ async fn test_get_or_create_user_by_github_account(db: &Arc<Database>) {
.user_id; .user_id;
let user = db let user = db
.get_or_create_user_by_github_account( .get_or_create_user_by_github_account("the-new-login2", 102, None, Some(Utc::now()), None)
"the-new-login2",
Some(102),
None,
Some(Utc::now()),
None,
)
.await .await
.unwrap(); .unwrap();
assert_eq!(user.id, user_id2); assert_eq!(user.id, user_id2);
assert_eq!(&user.github_login, "the-new-login2"); assert_eq!(&user.github_login, "the-new-login2");
assert_eq!(user.github_user_id, Some(102)); assert_eq!(user.github_user_id, 102);
let user = db let user = db
.get_or_create_user_by_github_account( .get_or_create_user_by_github_account(
"login3", "login3",
Some(103), 103,
Some("user3@example.com"), Some("user3@example.com"),
Some(Utc::now()), Some(Utc::now()),
None, None,
@ -125,7 +119,7 @@ async fn test_get_or_create_user_by_github_account(db: &Arc<Database>) {
.await .await
.unwrap(); .unwrap();
assert_eq!(&user.github_login, "login3"); assert_eq!(&user.github_login, "login3");
assert_eq!(user.github_user_id, Some(103)); assert_eq!(user.github_user_id, 103);
assert_eq!(user.email_address, Some("user3@example.com".into())); assert_eq!(user.email_address, Some("user3@example.com".into()));
} }

View file

@ -127,7 +127,7 @@ pub async fn seed(config: &Config, db: &Database, force: bool) -> anyhow::Result
let user = db let user = db
.get_or_create_user_by_github_account( .get_or_create_user_by_github_account(
&github_user.login, &github_user.login,
Some(github_user.id), github_user.id,
github_user.email.as_deref(), github_user.email.as_deref(),
None, None,
None, None,

View file

@ -168,7 +168,7 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
server server
.app_state .app_state
.db .db
.get_or_create_user_by_github_account("user_b", Some(100), None, Some(Utc::now()), None) .get_or_create_user_by_github_account("user_b", 100, None, Some(Utc::now()), None)
.await .await
.unwrap(); .unwrap();
@ -266,7 +266,7 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
server server
.app_state .app_state
.db .db
.add_contributor("user_b", Some(100), None, Some(Utc::now()), None) .add_contributor("user_b", 100, None, Some(Utc::now()), None)
.await .await
.unwrap(); .unwrap();

View file

@ -84,7 +84,7 @@ impl UserBackfiller {
self.db self.db
.get_or_create_user_by_github_account( .get_or_create_user_by_github_account(
&user.github_login, &user.github_login,
Some(github_user.id), github_user.id,
user.email_address.as_deref(), user.email_address.as_deref(),
Some(github_user.created_at), Some(github_user.created_at),
initial_channel_id, initial_channel_id,