From 1ff570ee3573ffd29c4f9129ae26e7f6e86f9154 Mon Sep 17 00:00:00 2001 From: "Carol (Nichols || Goulding)" Date: Mon, 17 Feb 2025 15:07:45 -0500 Subject: [PATCH 1/6] Duplicate GitHub OAuth info to a linked_accounts table (deploy 1) That has a `provider` column that will (for now) always be set to 0, which corresponds to `AccountProvider::Github`. The table's primary key is (provider, account_id), which corresponds to (0, gh_id). This constraint will mean a particular GitHub/GitLab/etc account, identified from the provider by an ID, may only be associated with one crates.io user record, but a crates.io user record could (eventually) have *both* a GitHub *and* a GitLab account associated with it (or two GitHub accounts, even!) This is the first step of many to eventually allow for crates.io accounts linked to other OAuth providers in addition/instead of GitHub. No code aside from one test is reading from the linked accounts table at this time. No backfill has been done yet. No handling of creating/associating multiple OAuth accounts with one crates.io account has been done yet. --- crates/crates_io_database/src/models/mod.rs | 2 +- crates/crates_io_database/src/models/user.rs | 79 ++++++++++++++++++- crates/crates_io_database/src/schema.rs | 46 +++++++++++ .../crates_io_database_dump/src/dump-db.toml | 10 +++ ...e_dump__tests__sql_scripts@export.sql.snap | 2 + ...e_dump__tests__sql_scripts@import.sql.snap | 7 ++ .../down.sql | 1 + .../up.sql | 9 +++ src/controllers/session.rs | 17 +++- src/tests/dump_db.rs | 2 + src/tests/user.rs | 47 ++++++++++- 11 files changed, 217 insertions(+), 5 deletions(-) create mode 100644 migrations/2025-01-29-205705_linked_accounts_table/down.sql create mode 100644 migrations/2025-01-29-205705_linked_accounts_table/up.sql diff --git a/crates/crates_io_database/src/models/mod.rs b/crates/crates_io_database/src/models/mod.rs index 2b6f2698adc..7b75204056a 100644 --- a/crates/crates_io_database/src/models/mod.rs +++ b/crates/crates_io_database/src/models/mod.rs @@ -14,7 +14,7 @@ pub use self::krate::{Crate, CrateName, NewCrate, RecentCrateDownloads}; pub use self::owner::{CrateOwner, Owner, OwnerKind}; pub use self::team::{NewTeam, Team}; pub use self::token::ApiToken; -pub use self::user::{NewUser, User}; +pub use self::user::{AccountProvider, LinkedAccount, NewLinkedAccount, NewUser, User}; pub use self::version::{NewVersion, TopVersions, Version}; pub mod helpers; diff --git a/crates/crates_io_database/src/models/user.rs b/crates/crates_io_database/src/models/user.rs index 9d060361ca5..48ec3d35b22 100644 --- a/crates/crates_io_database/src/models/user.rs +++ b/crates/crates_io_database/src/models/user.rs @@ -8,8 +8,8 @@ use diesel_async::{AsyncPgConnection, RunQueryDsl}; use secrecy::SecretString; use crate::models::{Crate, CrateOwner, Email, Owner, OwnerKind}; -use crate::schema::{crate_owners, emails, users}; -use crates_io_diesel_helpers::lower; +use crate::schema::{crate_owners, emails, linked_accounts, users}; +use crates_io_diesel_helpers::{lower, pg_enum}; /// The model representing a row in the `users` database table. #[derive(Clone, Debug, Queryable, Identifiable, Selectable)] @@ -122,3 +122,78 @@ impl NewUser<'_> { .await } } + +// Supported OAuth providers. Currently only GitHub. +pg_enum! { + pub enum AccountProvider { + Github = 0, + } +} + +/// Represents an OAuth account record linked to a user record. +#[derive(Associations, Identifiable, Selectable, Queryable, Debug, Clone)] +#[diesel( + table_name = linked_accounts, + check_for_backend(diesel::pg::Pg), + primary_key(provider, account_id), + belongs_to(User), +)] +pub struct LinkedAccount { + pub user_id: i32, + pub provider: AccountProvider, + pub account_id: i32, // corresponds to user.gh_id + #[diesel(deserialize_as = String)] + pub access_token: SecretString, // corresponds to user.gh_access_token + pub login: String, // corresponds to user.gh_login + pub avatar: Option, // corresponds to user.gh_avatar +} + +/// Represents a new linked account record insertable to the `linked_accounts` table +#[derive(Insertable, Debug, Builder)] +#[diesel( + table_name = linked_accounts, + check_for_backend(diesel::pg::Pg), + primary_key(provider, account_id), + belongs_to(User), +)] +pub struct NewLinkedAccount<'a> { + pub user_id: i32, + pub provider: AccountProvider, + pub account_id: i32, // corresponds to user.gh_id + pub access_token: &'a str, // corresponds to user.gh_access_token + pub login: &'a str, // corresponds to user.gh_login + pub avatar: Option<&'a str>, // corresponds to user.gh_avatar +} + +impl NewLinkedAccount<'_> { + /// Inserts the linked account into the database, or updates an existing one. + /// + /// This is to be used for logging in when there is no currently logged-in user, as opposed to + /// adding another linked account to a currently-logged-in user. The logic for adding another + /// linked account (when that ability gets added) will need to ensure that a particular + /// (provider, account_id) combo (ex: GitHub account with GitHub ID 1234) is only associated + /// with one crates.io account, so that we know what crates.io account to log in when we get an + /// oAuth request from GitHub ID 1234. In other words, we should NOT be updating the user_id on + /// an existing (provider, account_id) row when starting from a currently-logged-in crates.io \ + /// user because that would mean that oAuth account has already been associated with a + /// different crates.io account. + /// + /// This function should be called if there is no current user and should update, say, the + /// access token, login, or avatar if those have changed. + pub async fn insert_or_update( + &self, + conn: &mut AsyncPgConnection, + ) -> QueryResult { + diesel::insert_into(linked_accounts::table) + .values(self) + .on_conflict((linked_accounts::provider, linked_accounts::account_id)) + .do_update() + .set(( + linked_accounts::access_token.eq(excluded(linked_accounts::access_token)), + linked_accounts::login.eq(excluded(linked_accounts::login)), + linked_accounts::avatar.eq(excluded(linked_accounts::avatar)), + )) + .get_result(conn) + .await + } +} diff --git a/crates/crates_io_database/src/schema.rs b/crates/crates_io_database/src/schema.rs index 9b05b2434e1..bccecbb2fe3 100644 --- a/crates/crates_io_database/src/schema.rs +++ b/crates/crates_io_database/src/schema.rs @@ -593,6 +593,50 @@ diesel::table! { } } +diesel::table! { + /// Representation of the `linked_accounts` table. + /// + /// (Automatically generated by Diesel.) + linked_accounts (provider, account_id) { + /// The `user_id` column of the `linked_accounts` table. + /// + /// Its SQL type is `Int4`. + /// + /// (Automatically generated by Diesel.) + user_id -> Int4, + /// The `provider` column of the `linked_accounts` table. + /// + /// Its SQL type is `Int4`. + /// + /// (Automatically generated by Diesel.) + provider -> Int4, + /// The `account_id` column of the `linked_accounts` table. + /// + /// Its SQL type is `Int4`. + /// + /// (Automatically generated by Diesel.) + account_id -> Int4, + /// The `access_token` column of the `linked_accounts` table. + /// + /// Its SQL type is `Varchar`. + /// + /// (Automatically generated by Diesel.) + access_token -> Varchar, + /// The `login` column of the `linked_accounts` table. + /// + /// Its SQL type is `Varchar`. + /// + /// (Automatically generated by Diesel.) + login -> Varchar, + /// The `avatar` column of the `linked_accounts` table. + /// + /// Its SQL type is `Nullable`. + /// + /// (Automatically generated by Diesel.) + avatar -> Nullable, + } +} + diesel::table! { /// Representation of the `metadata` table. /// @@ -1066,6 +1110,7 @@ diesel::joinable!(dependencies -> versions (version_id)); diesel::joinable!(emails -> users (user_id)); diesel::joinable!(follows -> crates (crate_id)); diesel::joinable!(follows -> users (user_id)); +diesel::joinable!(linked_accounts -> users (user_id)); diesel::joinable!(publish_limit_buckets -> users (user_id)); diesel::joinable!(publish_rate_overrides -> users (user_id)); diesel::joinable!(readme_renderings -> versions (version_id)); @@ -1094,6 +1139,7 @@ diesel::allow_tables_to_appear_in_same_query!( emails, follows, keywords, + linked_accounts, metadata, processed_log_files, publish_limit_buckets, diff --git a/crates/crates_io_database_dump/src/dump-db.toml b/crates/crates_io_database_dump/src/dump-db.toml index e42d45c59dd..95199a90467 100644 --- a/crates/crates_io_database_dump/src/dump-db.toml +++ b/crates/crates_io_database_dump/src/dump-db.toml @@ -154,6 +154,16 @@ keyword = "public" crates_cnt = "public" created_at = "public" +[linked_accounts.columns] +user_id = "public" +provider = "public" +account_id = "public" +access_token = "private" +login = "public" +avatar = "public" +[linked_accounts.column_defaults] +access_token = "''" + [metadata.columns] total_downloads = "public" diff --git a/crates/crates_io_database_dump/src/snapshots/crates_io_database_dump__tests__sql_scripts@export.sql.snap b/crates/crates_io_database_dump/src/snapshots/crates_io_database_dump__tests__sql_scripts@export.sql.snap index 1d801f192d7..79b024b9d39 100644 --- a/crates/crates_io_database_dump/src/snapshots/crates_io_database_dump__tests__sql_scripts@export.sql.snap +++ b/crates/crates_io_database_dump/src/snapshots/crates_io_database_dump__tests__sql_scripts@export.sql.snap @@ -1,6 +1,7 @@ --- source: crates/crates_io_database_dump/src/lib.rs expression: content +snapshot_kind: text --- BEGIN ISOLATION LEVEL REPEATABLE READ, READ ONLY; @@ -8,6 +9,7 @@ BEGIN ISOLATION LEVEL REPEATABLE READ, READ ONLY; \copy "crate_downloads" ("crate_id", "downloads") TO 'data/crate_downloads.csv' WITH CSV HEADER \copy "crates" ("created_at", "description", "documentation", "homepage", "id", "max_features", "max_upload_size", "name", "readme", "repository", "updated_at") TO 'data/crates.csv' WITH CSV HEADER \copy "keywords" ("crates_cnt", "created_at", "id", "keyword") TO 'data/keywords.csv' WITH CSV HEADER + \copy "linked_accounts" ("account_id", "avatar", "login", "provider", "user_id") TO 'data/linked_accounts.csv' WITH CSV HEADER \copy "metadata" ("total_downloads") TO 'data/metadata.csv' WITH CSV HEADER \copy "reserved_crate_names" ("name") TO 'data/reserved_crate_names.csv' WITH CSV HEADER \copy "teams" ("avatar", "github_id", "id", "login", "name", "org_id") TO 'data/teams.csv' WITH CSV HEADER diff --git a/crates/crates_io_database_dump/src/snapshots/crates_io_database_dump__tests__sql_scripts@import.sql.snap b/crates/crates_io_database_dump/src/snapshots/crates_io_database_dump__tests__sql_scripts@import.sql.snap index f5315ad6929..6da832e4c46 100644 --- a/crates/crates_io_database_dump/src/snapshots/crates_io_database_dump__tests__sql_scripts@import.sql.snap +++ b/crates/crates_io_database_dump/src/snapshots/crates_io_database_dump__tests__sql_scripts@import.sql.snap @@ -1,6 +1,7 @@ --- source: crates/crates_io_database_dump/src/lib.rs expression: content +snapshot_kind: text --- BEGIN; -- Disable triggers on each table. @@ -9,6 +10,7 @@ BEGIN; ALTER TABLE "crate_downloads" DISABLE TRIGGER ALL; ALTER TABLE "crates" DISABLE TRIGGER ALL; ALTER TABLE "keywords" DISABLE TRIGGER ALL; + ALTER TABLE "linked_accounts" DISABLE TRIGGER ALL; ALTER TABLE "metadata" DISABLE TRIGGER ALL; ALTER TABLE "reserved_crate_names" DISABLE TRIGGER ALL; ALTER TABLE "teams" DISABLE TRIGGER ALL; @@ -23,6 +25,7 @@ BEGIN; -- Set defaults for non-nullable columns not included in the dump. + ALTER TABLE "linked_accounts" ALTER COLUMN "access_token" SET DEFAULT ''; ALTER TABLE "users" ALTER COLUMN "gh_access_token" SET DEFAULT ''; -- Truncate all tables. @@ -31,6 +34,7 @@ BEGIN; TRUNCATE "crate_downloads" RESTART IDENTITY CASCADE; TRUNCATE "crates" RESTART IDENTITY CASCADE; TRUNCATE "keywords" RESTART IDENTITY CASCADE; + TRUNCATE "linked_accounts" RESTART IDENTITY CASCADE; TRUNCATE "metadata" RESTART IDENTITY CASCADE; TRUNCATE "reserved_crate_names" RESTART IDENTITY CASCADE; TRUNCATE "teams" RESTART IDENTITY CASCADE; @@ -52,6 +56,7 @@ BEGIN; \copy "crate_downloads" ("crate_id", "downloads") FROM 'data/crate_downloads.csv' WITH CSV HEADER \copy "crates" ("created_at", "description", "documentation", "homepage", "id", "max_features", "max_upload_size", "name", "readme", "repository", "updated_at") FROM 'data/crates.csv' WITH CSV HEADER \copy "keywords" ("crates_cnt", "created_at", "id", "keyword") FROM 'data/keywords.csv' WITH CSV HEADER + \copy "linked_accounts" ("account_id", "avatar", "login", "provider", "user_id") FROM 'data/linked_accounts.csv' WITH CSV HEADER \copy "metadata" ("total_downloads") FROM 'data/metadata.csv' WITH CSV HEADER \copy "reserved_crate_names" ("name") FROM 'data/reserved_crate_names.csv' WITH CSV HEADER \copy "teams" ("avatar", "github_id", "id", "login", "name", "org_id") FROM 'data/teams.csv' WITH CSV HEADER @@ -66,6 +71,7 @@ BEGIN; -- Drop the defaults again. + ALTER TABLE "linked_accounts" ALTER COLUMN "access_token" DROP DEFAULT; ALTER TABLE "users" ALTER COLUMN "gh_access_token" DROP DEFAULT; -- Reenable triggers on each table. @@ -74,6 +80,7 @@ BEGIN; ALTER TABLE "crate_downloads" ENABLE TRIGGER ALL; ALTER TABLE "crates" ENABLE TRIGGER ALL; ALTER TABLE "keywords" ENABLE TRIGGER ALL; + ALTER TABLE "linked_accounts" ENABLE TRIGGER ALL; ALTER TABLE "metadata" ENABLE TRIGGER ALL; ALTER TABLE "reserved_crate_names" ENABLE TRIGGER ALL; ALTER TABLE "teams" ENABLE TRIGGER ALL; diff --git a/migrations/2025-01-29-205705_linked_accounts_table/down.sql b/migrations/2025-01-29-205705_linked_accounts_table/down.sql new file mode 100644 index 00000000000..bef2a0aa5ea --- /dev/null +++ b/migrations/2025-01-29-205705_linked_accounts_table/down.sql @@ -0,0 +1 @@ +DROP TABLE linked_accounts; diff --git a/migrations/2025-01-29-205705_linked_accounts_table/up.sql b/migrations/2025-01-29-205705_linked_accounts_table/up.sql new file mode 100644 index 00000000000..aa2d79a9fcc --- /dev/null +++ b/migrations/2025-01-29-205705_linked_accounts_table/up.sql @@ -0,0 +1,9 @@ +CREATE TABLE linked_accounts ( + user_id INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, + provider INTEGER NOT NULL, + account_id INTEGER NOT NULL, + access_token VARCHAR NOT NULL, + login VARCHAR NOT NULL, + avatar VARCHAR, + PRIMARY KEY (provider, account_id) +); diff --git a/src/controllers/session.rs b/src/controllers/session.rs index 44266c356a3..a1deda256de 100644 --- a/src/controllers/session.rs +++ b/src/controllers/session.rs @@ -10,7 +10,7 @@ use crate::app::AppState; use crate::controllers::user::update::UserConfirmEmail; use crate::email::Emails; use crate::middleware::log_request::RequestLogExt; -use crate::models::{NewEmail, NewUser, User}; +use crate::models::{AccountProvider, NewEmail, NewLinkedAccount, NewUser, User}; use crate::schema::users; use crate::util::diesel::is_read_only_error; use crate::util::errors::{AppResult, bad_request, server_error}; @@ -162,6 +162,21 @@ async fn create_or_update_user( async move { let user = new_user.insert_or_update(conn).await?; + // To assist in eventually someday allowing OAuth with more than GitHub, also + // write the GitHub info to the `linked_accounts` table. Nothing currently reads + // from this table. Only log errors but don't fail login if this writing fails. + let new_linked_account = NewLinkedAccount::builder() + .user_id(user.id) + .provider(AccountProvider::Github) + .account_id(user.gh_id) + .access_token(new_user.gh_access_token) + .login(&user.gh_login) + .maybe_avatar(user.gh_avatar.as_deref()) + .build(); + if let Err(e) = new_linked_account.insert_or_update(conn).await { + info!("Error inserting or updating linked_accounts record: {e}"); + } + // To send the user an account verification email if let Some(user_email) = email { let new_email = NewEmail::builder() diff --git a/src/tests/dump_db.rs b/src/tests/dump_db.rs index c7d0e549ac8..bce2be568e8 100644 --- a/src/tests/dump_db.rs +++ b/src/tests/dump_db.rs @@ -52,6 +52,7 @@ async fn test_dump_db_job() -> anyhow::Result<()> { "YYYY-MM-DD-HHMMSS/data/crate_downloads.csv", "YYYY-MM-DD-HHMMSS/data/crates.csv", "YYYY-MM-DD-HHMMSS/data/keywords.csv", + "YYYY-MM-DD-HHMMSS/data/linked_accounts.csv", "YYYY-MM-DD-HHMMSS/data/metadata.csv", "YYYY-MM-DD-HHMMSS/data/reserved_crate_names.csv", "YYYY-MM-DD-HHMMSS/data/teams.csv", @@ -84,6 +85,7 @@ async fn test_dump_db_job() -> anyhow::Result<()> { "data/crate_downloads.csv", "data/crates.csv", "data/keywords.csv", + "data/linked_accounts.csv", "data/metadata.csv", "data/reserved_crate_names.csv", "data/teams.csv", diff --git a/src/tests/user.rs b/src/tests/user.rs index 33e98796849..6e9cb690229 100644 --- a/src/tests/user.rs +++ b/src/tests/user.rs @@ -1,5 +1,6 @@ use crate::controllers::session; -use crate::models::{ApiToken, Email, User}; +use crate::models::{AccountProvider, ApiToken, Email, LinkedAccount, User}; +use crate::schema::linked_accounts; use crate::tests::TestApp; use crate::tests::util::github::next_gh_id; use crate::tests::util::{MockCookieUser, RequestHelper}; @@ -265,3 +266,47 @@ async fn test_existing_user_email() -> anyhow::Result<()> { Ok(()) } + +// To assist in eventually someday allowing OAuth with more than GitHub, verify that we're starting +// to also write the GitHub info to the `linked_accounts` table. Nothing currently reads from this +// table other than this test. +#[tokio::test(flavor = "multi_thread")] +async fn also_write_to_linked_accounts() -> anyhow::Result<()> { + let (app, _) = TestApp::init().empty().await; + let mut conn = app.db_conn().await; + + // Simulate logging in via GitHub. Don't use app.db_new_user because it inserts a user record + // directly into the database and we want to test the OAuth flow here. + let email = "potahto@example.com"; + + let emails = &app.as_inner().emails; + + let gh_user = GithubUser { + id: next_gh_id(), + login: "arbitrary_username".to_string(), + name: None, + email: Some(email.to_string()), + avatar_url: None, + }; + + let u = + session::save_user_to_database(&gh_user, "some random token", emails, &mut conn).await?; + + let linked_accounts = linked_accounts::table + .filter(linked_accounts::provider.eq(AccountProvider::Github)) + .filter(linked_accounts::account_id.eq(u.gh_id)) + .load::(&mut conn) + .await + .unwrap(); + + assert_eq!(linked_accounts.len(), 1); + let linked_account = &linked_accounts[0]; + assert_eq!(linked_account.user_id, u.id); + assert_eq!(linked_account.login, u.gh_login); + assert_eq!( + linked_account.access_token.expose_secret(), + u.gh_access_token.expose_secret() + ); + + Ok(()) +} From 7ba45327b5040266ec359554829a1d134922f7f0 Mon Sep 17 00:00:00 2001 From: "Carol (Nichols || Goulding)" Date: Thu, 20 Feb 2025 21:54:11 -0500 Subject: [PATCH 2/6] (untested) SQL to backfill linked_accounts (deploy 2) --- script/backfill-linked-accounts.sql | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 script/backfill-linked-accounts.sql diff --git a/script/backfill-linked-accounts.sql b/script/backfill-linked-accounts.sql new file mode 100644 index 00000000000..f2413cd110e --- /dev/null +++ b/script/backfill-linked-accounts.sql @@ -0,0 +1,7 @@ +INSERT INTO linked_accounts (user_id, provider, account_id, access_token, login, avatar) +SELECT id, 0, gh_id, gh_access_token, gh_login, gh_avatar +FROM users +LEFT JOIN linked_accounts +ON users.id = linked_accounts.user_id +WHERE gh_id != -1 +AND linked_accounts.user_id IS NULL; From e2116b3201afdfcbe6f161c58cb89f86b99b1455 Mon Sep 17 00:00:00 2001 From: "Carol (Nichols || Goulding)" Date: Tue, 18 Feb 2025 21:11:18 -0500 Subject: [PATCH 3/6] Add a username field to the users table (deploy 3?) Right now, this will always have the same value as gh_login on the users table and login on the linked accounts table. All of these values will get updated when we get a new gh_login value. Eventually, we're going to have to decouple the concept of crates.io "username" from the logins of the various account(s), which you may or may not want to update when you update your GitHub or other login, and which may or may not conflict with other users' crates.io usernames. But for now, set up for that future and defer the harder decisions until later by making this field always get the same value as gh_login. This adds a `username` field to the JSON views returned from the API but does not use that field anywhere yet. Question: should team owner JSON also have a field named `username` as done in this commit? it's a little weird for a team to have a username because it's not a user. but it's consistent. something more generic like `name` might also be confusing. something more specific like `crates_io_identifier` is more verbose but less confusable. shrug --- crates/crates_io_database/src/models/user.rs | 3 ++ crates/crates_io_database/src/schema.rs | 6 +++ .../crates_io_database_dump/src/dump-db.toml | 1 + ...e_dump__tests__sql_scripts@export.sql.snap | 2 +- ...e_dump__tests__sql_scripts@import.sql.snap | 2 +- .../down.sql | 4 ++ .../up.sql | 6 +++ src/controllers/crate_owner_invitation.rs | 2 +- src/controllers/session.rs | 1 + src/index.rs | 1 + src/rate_limiter.rs | 1 + ..._io__openapi__tests__openapi_snapshot.snap | 25 +++++++++-- ..._publish__edition__edition_is_saved-2.snap | 7 ++- ...lish__links__crate_with_links_field-2.snap | 7 ++- ...__publish__manifest__boolean_readme-2.snap | 7 ++- ...ublish__manifest__lib_and_bin_crate-2.snap | 7 ++- ..._yanking__patch_version_yank_unyank-2.snap | 4 ++ ..._yanking__patch_version_yank_unyank-3.snap | 5 +++ ..._yanking__patch_version_yank_unyank-4.snap | 5 +++ ..._yanking__patch_version_yank_unyank-5.snap | 6 +++ ..._yanking__patch_version_yank_unyank-6.snap | 6 +++ ...e__yanking__patch_version_yank_unyank.snap | 4 ++ src/tests/mod.rs | 1 + src/tests/owners.rs | 1 + ...crates__read__include_default_version.snap | 4 +- ...io__tests__routes__crates__read__show.snap | 7 ++- ...routes__crates__read__show_all_yanked.snap | 7 ++- ..._not_included_in_reverse_dependencies.snap | 4 +- ...se_dependencies__reverse_dependencies.snap | 4 +- ...cludes_published_by_user_when_present.snap | 4 +- ...ery_supports_u64_version_number_parts.snap | 4 +- ...ld_version_doesnt_depend_but_new_does.snap | 4 +- ..._not_included_in_reverse_dependencies.snap | 4 +- ...tes__crates__versions__list__versions.snap | 7 ++- ..._read__show_by_crate_name_and_version.snap | 4 +- ...ates_io__tests__routes__me__get__me-2.snap | 4 +- ...ates_io__tests__routes__me__get__me-3.snap | 4 +- src/tests/routes/me/updates.rs | 4 +- src/tests/routes/users/read.rs | 2 + src/tests/user.rs | 1 + src/typosquat/test_util.rs | 1 + src/views.rs | 45 +++++++++++++++---- src/worker/jobs/downloads/update_metadata.rs | 1 + src/worker/jobs/expiry_notification.rs | 1 + 44 files changed, 190 insertions(+), 40 deletions(-) create mode 100644 migrations/2025-02-19-013433_add_username_to_user/down.sql create mode 100644 migrations/2025-02-19-013433_add_username_to_user/up.sql diff --git a/crates/crates_io_database/src/models/user.rs b/crates/crates_io_database/src/models/user.rs index 48ec3d35b22..b7b8511caca 100644 --- a/crates/crates_io_database/src/models/user.rs +++ b/crates/crates_io_database/src/models/user.rs @@ -25,6 +25,7 @@ pub struct User { pub account_lock_until: Option>, pub is_admin: bool, pub publish_notifications: bool, + pub username: Option, } impl User { @@ -85,6 +86,7 @@ pub struct NewUser<'a> { pub gh_id: i32, pub gh_login: &'a str, pub name: Option<&'a str>, + pub username: Option<&'a str>, pub gh_avatar: Option<&'a str>, pub gh_access_token: &'a str, } @@ -114,6 +116,7 @@ impl NewUser<'_> { .do_update() .set(( users::gh_login.eq(excluded(users::gh_login)), + users::username.eq(excluded(users::username)), users::name.eq(excluded(users::name)), users::gh_avatar.eq(excluded(users::gh_avatar)), users::gh_access_token.eq(excluded(users::gh_access_token)), diff --git a/crates/crates_io_database/src/schema.rs b/crates/crates_io_database/src/schema.rs index bccecbb2fe3..ccf182b95e6 100644 --- a/crates/crates_io_database/src/schema.rs +++ b/crates/crates_io_database/src/schema.rs @@ -870,6 +870,12 @@ diesel::table! { is_admin -> Bool, /// Whether or not the user wants to receive notifications when a package they own is published publish_notifications -> Bool, + /// The `username` column of the `users` table. + /// + /// Its SQL type is `Nullable`. + /// + /// (Automatically generated by Diesel.) + username -> Nullable, } } diff --git a/crates/crates_io_database_dump/src/dump-db.toml b/crates/crates_io_database_dump/src/dump-db.toml index 95199a90467..9213d7ad5c1 100644 --- a/crates/crates_io_database_dump/src/dump-db.toml +++ b/crates/crates_io_database_dump/src/dump-db.toml @@ -216,6 +216,7 @@ account_lock_reason = "private" account_lock_until = "private" is_admin = "private" publish_notifications = "private" +username = "public" [users.column_defaults] gh_access_token = "''" diff --git a/crates/crates_io_database_dump/src/snapshots/crates_io_database_dump__tests__sql_scripts@export.sql.snap b/crates/crates_io_database_dump/src/snapshots/crates_io_database_dump__tests__sql_scripts@export.sql.snap index 79b024b9d39..41039804ec0 100644 --- a/crates/crates_io_database_dump/src/snapshots/crates_io_database_dump__tests__sql_scripts@export.sql.snap +++ b/crates/crates_io_database_dump/src/snapshots/crates_io_database_dump__tests__sql_scripts@export.sql.snap @@ -13,7 +13,7 @@ BEGIN ISOLATION LEVEL REPEATABLE READ, READ ONLY; \copy "metadata" ("total_downloads") TO 'data/metadata.csv' WITH CSV HEADER \copy "reserved_crate_names" ("name") TO 'data/reserved_crate_names.csv' WITH CSV HEADER \copy "teams" ("avatar", "github_id", "id", "login", "name", "org_id") TO 'data/teams.csv' WITH CSV HEADER - \copy (SELECT "gh_avatar", "gh_id", "gh_login", "id", "name" FROM "users" WHERE id in ( SELECT owner_id AS user_id FROM crate_owners WHERE NOT deleted AND owner_kind = 0 UNION SELECT published_by as user_id FROM versions )) TO 'data/users.csv' WITH CSV HEADER + \copy (SELECT "gh_avatar", "gh_id", "gh_login", "id", "name", "username" FROM "users" WHERE id in ( SELECT owner_id AS user_id FROM crate_owners WHERE NOT deleted AND owner_kind = 0 UNION SELECT published_by as user_id FROM versions )) TO 'data/users.csv' WITH CSV HEADER \copy "crates_categories" ("category_id", "crate_id") TO 'data/crates_categories.csv' WITH CSV HEADER \copy "crates_keywords" ("crate_id", "keyword_id") TO 'data/crates_keywords.csv' WITH CSV HEADER diff --git a/crates/crates_io_database_dump/src/snapshots/crates_io_database_dump__tests__sql_scripts@import.sql.snap b/crates/crates_io_database_dump/src/snapshots/crates_io_database_dump__tests__sql_scripts@import.sql.snap index 6da832e4c46..20a067d5905 100644 --- a/crates/crates_io_database_dump/src/snapshots/crates_io_database_dump__tests__sql_scripts@import.sql.snap +++ b/crates/crates_io_database_dump/src/snapshots/crates_io_database_dump__tests__sql_scripts@import.sql.snap @@ -60,7 +60,7 @@ BEGIN; \copy "metadata" ("total_downloads") FROM 'data/metadata.csv' WITH CSV HEADER \copy "reserved_crate_names" ("name") FROM 'data/reserved_crate_names.csv' WITH CSV HEADER \copy "teams" ("avatar", "github_id", "id", "login", "name", "org_id") FROM 'data/teams.csv' WITH CSV HEADER - \copy "users" ("gh_avatar", "gh_id", "gh_login", "id", "name") FROM 'data/users.csv' WITH CSV HEADER + \copy "users" ("gh_avatar", "gh_id", "gh_login", "id", "name", "username") FROM 'data/users.csv' WITH CSV HEADER \copy "crates_categories" ("category_id", "crate_id") FROM 'data/crates_categories.csv' WITH CSV HEADER \copy "crates_keywords" ("crate_id", "keyword_id") FROM 'data/crates_keywords.csv' WITH CSV HEADER \copy "crate_owners" ("crate_id", "created_at", "created_by", "owner_id", "owner_kind") FROM 'data/crate_owners.csv' WITH CSV HEADER diff --git a/migrations/2025-02-19-013433_add_username_to_user/down.sql b/migrations/2025-02-19-013433_add_username_to_user/down.sql new file mode 100644 index 00000000000..228952f6bf6 --- /dev/null +++ b/migrations/2025-02-19-013433_add_username_to_user/down.sql @@ -0,0 +1,4 @@ +DROP INDEX IF EXISTS lower_username; + +ALTER TABLE users +DROP COLUMN username; diff --git a/migrations/2025-02-19-013433_add_username_to_user/up.sql b/migrations/2025-02-19-013433_add_username_to_user/up.sql new file mode 100644 index 00000000000..661c0587e8d --- /dev/null +++ b/migrations/2025-02-19-013433_add_username_to_user/up.sql @@ -0,0 +1,6 @@ +ALTER TABLE users +-- The column needs to be nullable for this migration to be fast; can be changed to non-nullable +-- after backfill of all records. +ADD COLUMN username VARCHAR; + +CREATE INDEX lower_username ON users (lower(username)); diff --git a/src/controllers/crate_owner_invitation.rs b/src/controllers/crate_owner_invitation.rs index 24547c0e650..6fd88928e14 100644 --- a/src/controllers/crate_owner_invitation.rs +++ b/src/controllers/crate_owner_invitation.rs @@ -63,7 +63,7 @@ pub async fn list_crate_owner_invitations_for_user( .iter() .find(|u| u.id == private.inviter_id) .ok_or_else(|| internal(format!("missing user {}", private.inviter_id)))? - .login + .username .clone(), invitee_id: private.invitee_id, inviter_id: private.inviter_id, diff --git a/src/controllers/session.rs b/src/controllers/session.rs index a1deda256de..8dd53424e04 100644 --- a/src/controllers/session.rs +++ b/src/controllers/session.rs @@ -132,6 +132,7 @@ pub async fn save_user_to_database( let new_user = NewUser::builder() .gh_id(user.id) .gh_login(&user.login) + .username(&user.login) .maybe_name(user.name.as_deref()) .maybe_gh_avatar(user.avatar_url.as_deref()) .gh_access_token(access_token) diff --git a/src/index.rs b/src/index.rs index 0e52eb91962..08eacf2266a 100644 --- a/src/index.rs +++ b/src/index.rs @@ -153,6 +153,7 @@ mod tests { let user_id = diesel::insert_into(users::table) .values(( users::name.eq("user1"), + users::username.eq("user1"), users::gh_login.eq("user1"), users::gh_id.eq(42), users::gh_access_token.eq("some random token"), diff --git a/src/rate_limiter.rs b/src/rate_limiter.rs index 5416a9ffaa4..4b4da12db2c 100644 --- a/src/rate_limiter.rs +++ b/src/rate_limiter.rs @@ -706,6 +706,7 @@ mod tests { NewUser::builder() .gh_id(0) .gh_login(gh_login) + .username(gh_login) .gh_access_token("some random token") .build() .insert(conn) diff --git a/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap b/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap index cb7a074036a..049a91a098b 100644 --- a/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap +++ b/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap @@ -1,6 +1,7 @@ --- source: src/openapi.rs expression: response.json() +snapshot_kind: text --- { "components": { @@ -117,7 +118,7 @@ expression: response.json() "type": "boolean" }, "login": { - "description": "The user's login name.", + "description": "The user's GitHub login.", "example": "ghost", "type": "string" }, @@ -141,11 +142,17 @@ expression: response.json() "string", "null" ] + }, + "username": { + "description": "The user's crates.io username.", + "example": "ghost", + "type": "string" } }, "required": [ "id", "login", + "username", "email_verified", "email_verification_sent", "is_admin", @@ -707,7 +714,7 @@ expression: response.json() "type": "string" }, "login": { - "description": "The login name of the team or user.", + "description": "The GitHub login of the team or user.", "example": "ghost", "type": "string" }, @@ -726,11 +733,17 @@ expression: response.json() "string", "null" ] + }, + "username": { + "description": "The crates.io username of the team or user.", + "example": "ghost", + "type": "string" } }, "required": [ "id", "login", + "username", "kind" ], "type": "object" @@ -853,7 +866,7 @@ expression: response.json() "type": "integer" }, "login": { - "description": "The user's login name.", + "description": "The user's GitHub login name.", "example": "ghost", "type": "string" }, @@ -869,11 +882,17 @@ expression: response.json() "description": "The user's GitHub profile URL.", "example": "https://github.com/ghost", "type": "string" + }, + "username": { + "description": "The user's crates.io username.", + "example": "ghost", + "type": "string" } }, "required": [ "id", "login", + "username", "url" ], "type": "object" diff --git a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__edition__edition_is_saved-2.snap b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__edition__edition_is_saved-2.snap index 02666658d2f..ce18851935a 100644 --- a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__edition__edition_is_saved-2.snap +++ b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__edition__edition_is_saved-2.snap @@ -1,6 +1,7 @@ --- source: src/tests/krate/publish/edition.rs expression: response.json() +snapshot_kind: text --- { "version": { @@ -13,7 +14,8 @@ expression: response.json() "id": "[id]", "login": "foo", "name": null, - "url": "https://github.com/foo" + "url": "https://github.com/foo", + "username": "foo" } } ], @@ -44,7 +46,8 @@ expression: response.json() "id": "[id]", "login": "foo", "name": null, - "url": "https://github.com/foo" + "url": "https://github.com/foo", + "username": "foo" }, "readme_path": "/api/v1/crates/foo/1.0.0/readme", "repository": null, diff --git a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__links__crate_with_links_field-2.snap b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__links__crate_with_links_field-2.snap index e42da14510a..3c046059c4c 100644 --- a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__links__crate_with_links_field-2.snap +++ b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__links__crate_with_links_field-2.snap @@ -1,6 +1,7 @@ --- source: src/tests/krate/publish/links.rs expression: response.json() +snapshot_kind: text --- { "version": { @@ -13,7 +14,8 @@ expression: response.json() "id": 1, "login": "foo", "name": null, - "url": "https://github.com/foo" + "url": "https://github.com/foo", + "username": "foo" } } ], @@ -44,7 +46,8 @@ expression: response.json() "id": "[id]", "login": "foo", "name": null, - "url": "https://github.com/foo" + "url": "https://github.com/foo", + "username": "foo" }, "readme_path": "/api/v1/crates/foo/1.0.0/readme", "repository": null, diff --git a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__boolean_readme-2.snap b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__boolean_readme-2.snap index 6f61d116c53..6b939f6d35c 100644 --- a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__boolean_readme-2.snap +++ b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__boolean_readme-2.snap @@ -1,6 +1,7 @@ --- source: src/tests/krate/publish/manifest.rs expression: response.json() +snapshot_kind: text --- { "version": { @@ -13,7 +14,8 @@ expression: response.json() "id": "[id]", "login": "foo", "name": null, - "url": "https://github.com/foo" + "url": "https://github.com/foo", + "username": "foo" } } ], @@ -44,7 +46,8 @@ expression: response.json() "id": "[id]", "login": "foo", "name": null, - "url": "https://github.com/foo" + "url": "https://github.com/foo", + "username": "foo" }, "readme_path": "/api/v1/crates/foo/1.0.0/readme", "repository": null, diff --git a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__lib_and_bin_crate-2.snap b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__lib_and_bin_crate-2.snap index 328af1f7ec4..070885d4fca 100644 --- a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__lib_and_bin_crate-2.snap +++ b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__lib_and_bin_crate-2.snap @@ -1,6 +1,7 @@ --- source: src/tests/krate/publish/manifest.rs expression: response.json() +snapshot_kind: text --- { "version": { @@ -13,7 +14,8 @@ expression: response.json() "id": "[id]", "login": "foo", "name": null, - "url": "https://github.com/foo" + "url": "https://github.com/foo", + "username": "foo" } } ], @@ -47,7 +49,8 @@ expression: response.json() "id": "[id]", "login": "foo", "name": null, - "url": "https://github.com/foo" + "url": "https://github.com/foo", + "username": "foo" }, "readme_path": "/api/v1/crates/foo/1.0.0/readme", "repository": null, diff --git a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-2.snap b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-2.snap index 9e8b8ef7e78..6bd08f4220f 100644 --- a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-2.snap +++ b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-2.snap @@ -1,6 +1,7 @@ --- source: src/tests/krate/yanking.rs expression: json +snapshot_kind: text --- { "version": { @@ -26,6 +27,7 @@ expression: json "published_by": { "id": 1, "login": "foo", + "username": "foo", "name": null, "avatar": null, "url": "https://github.com/foo" @@ -36,6 +38,7 @@ expression: json "user": { "id": 1, "login": "foo", + "username": "foo", "name": null, "avatar": null, "url": "https://github.com/foo" @@ -47,6 +50,7 @@ expression: json "user": { "id": 1, "login": "foo", + "username": "foo", "name": null, "avatar": null, "url": "https://github.com/foo" diff --git a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-3.snap b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-3.snap index 896ce3ae55b..3cc88906683 100644 --- a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-3.snap +++ b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-3.snap @@ -1,6 +1,7 @@ --- source: src/tests/krate/yanking.rs expression: json +snapshot_kind: text --- { "version": { @@ -26,6 +27,7 @@ expression: json "published_by": { "id": 1, "login": "foo", + "username": "foo", "name": null, "avatar": null, "url": "https://github.com/foo" @@ -36,6 +38,7 @@ expression: json "user": { "id": 1, "login": "foo", + "username": "foo", "name": null, "avatar": null, "url": "https://github.com/foo" @@ -47,6 +50,7 @@ expression: json "user": { "id": 1, "login": "foo", + "username": "foo", "name": null, "avatar": null, "url": "https://github.com/foo" @@ -58,6 +62,7 @@ expression: json "user": { "id": 1, "login": "foo", + "username": "foo", "name": null, "avatar": null, "url": "https://github.com/foo" diff --git a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-4.snap b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-4.snap index 896ce3ae55b..3cc88906683 100644 --- a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-4.snap +++ b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-4.snap @@ -1,6 +1,7 @@ --- source: src/tests/krate/yanking.rs expression: json +snapshot_kind: text --- { "version": { @@ -26,6 +27,7 @@ expression: json "published_by": { "id": 1, "login": "foo", + "username": "foo", "name": null, "avatar": null, "url": "https://github.com/foo" @@ -36,6 +38,7 @@ expression: json "user": { "id": 1, "login": "foo", + "username": "foo", "name": null, "avatar": null, "url": "https://github.com/foo" @@ -47,6 +50,7 @@ expression: json "user": { "id": 1, "login": "foo", + "username": "foo", "name": null, "avatar": null, "url": "https://github.com/foo" @@ -58,6 +62,7 @@ expression: json "user": { "id": 1, "login": "foo", + "username": "foo", "name": null, "avatar": null, "url": "https://github.com/foo" diff --git a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-5.snap b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-5.snap index e6938557e70..2a931ebc0d9 100644 --- a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-5.snap +++ b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-5.snap @@ -1,6 +1,7 @@ --- source: src/tests/krate/yanking.rs expression: json +snapshot_kind: text --- { "version": { @@ -26,6 +27,7 @@ expression: json "published_by": { "id": 1, "login": "foo", + "username": "foo", "name": null, "avatar": null, "url": "https://github.com/foo" @@ -36,6 +38,7 @@ expression: json "user": { "id": 1, "login": "foo", + "username": "foo", "name": null, "avatar": null, "url": "https://github.com/foo" @@ -47,6 +50,7 @@ expression: json "user": { "id": 1, "login": "foo", + "username": "foo", "name": null, "avatar": null, "url": "https://github.com/foo" @@ -58,6 +62,7 @@ expression: json "user": { "id": 1, "login": "foo", + "username": "foo", "name": null, "avatar": null, "url": "https://github.com/foo" @@ -69,6 +74,7 @@ expression: json "user": { "id": 1, "login": "foo", + "username": "foo", "name": null, "avatar": null, "url": "https://github.com/foo" diff --git a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-6.snap b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-6.snap index e6938557e70..2a931ebc0d9 100644 --- a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-6.snap +++ b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-6.snap @@ -1,6 +1,7 @@ --- source: src/tests/krate/yanking.rs expression: json +snapshot_kind: text --- { "version": { @@ -26,6 +27,7 @@ expression: json "published_by": { "id": 1, "login": "foo", + "username": "foo", "name": null, "avatar": null, "url": "https://github.com/foo" @@ -36,6 +38,7 @@ expression: json "user": { "id": 1, "login": "foo", + "username": "foo", "name": null, "avatar": null, "url": "https://github.com/foo" @@ -47,6 +50,7 @@ expression: json "user": { "id": 1, "login": "foo", + "username": "foo", "name": null, "avatar": null, "url": "https://github.com/foo" @@ -58,6 +62,7 @@ expression: json "user": { "id": 1, "login": "foo", + "username": "foo", "name": null, "avatar": null, "url": "https://github.com/foo" @@ -69,6 +74,7 @@ expression: json "user": { "id": 1, "login": "foo", + "username": "foo", "name": null, "avatar": null, "url": "https://github.com/foo" diff --git a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank.snap b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank.snap index 9e8b8ef7e78..6bd08f4220f 100644 --- a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank.snap +++ b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank.snap @@ -1,6 +1,7 @@ --- source: src/tests/krate/yanking.rs expression: json +snapshot_kind: text --- { "version": { @@ -26,6 +27,7 @@ expression: json "published_by": { "id": 1, "login": "foo", + "username": "foo", "name": null, "avatar": null, "url": "https://github.com/foo" @@ -36,6 +38,7 @@ expression: json "user": { "id": 1, "login": "foo", + "username": "foo", "name": null, "avatar": null, "url": "https://github.com/foo" @@ -47,6 +50,7 @@ expression: json "user": { "id": 1, "login": "foo", + "username": "foo", "name": null, "avatar": null, "url": "https://github.com/foo" diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 9d0ee1983a3..5b642f63278 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -95,6 +95,7 @@ fn new_user(login: &str) -> NewUser<'_> { NewUser::builder() .gh_id(next_gh_id()) .gh_login(login) + .username(login) .gh_access_token("some random token") .build() } diff --git a/src/tests/owners.rs b/src/tests/owners.rs index c0c7f892a3d..51870813cc1 100644 --- a/src/tests/owners.rs +++ b/src/tests/owners.rs @@ -772,6 +772,7 @@ async fn inactive_users_dont_get_invitations() { NewUser::builder() .gh_id(-1) .gh_login(invited_gh_login) + .username(invited_gh_login) .gh_access_token("some random token") .build() .insert(&mut conn) diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__include_default_version.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__include_default_version.snap index c7b079c37a4..a06971a8993 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__include_default_version.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__include_default_version.snap @@ -1,6 +1,7 @@ --- source: src/tests/routes/crates/read.rs expression: response.json() +snapshot_kind: text --- { "categories": null, @@ -66,7 +67,8 @@ expression: response.json() "id": 1, "login": "foo", "name": null, - "url": "https://github.com/foo" + "url": "https://github.com/foo", + "username": "foo" }, "readme_path": "/api/v1/crates/foo_default_version/0.5.1/readme", "repository": null, diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show.snap index 75c2cd698be..a4a002164e6 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show.snap @@ -1,6 +1,7 @@ --- source: src/tests/routes/crates/read.rs expression: response.json() +snapshot_kind: text --- { "categories": [], @@ -79,7 +80,8 @@ expression: response.json() "id": 1, "login": "foo", "name": null, - "url": "https://github.com/foo" + "url": "https://github.com/foo", + "username": "foo" }, "readme_path": "/api/v1/crates/foo_show/0.5.1/readme", "repository": null, @@ -117,7 +119,8 @@ expression: response.json() "id": 1, "login": "foo", "name": null, - "url": "https://github.com/foo" + "url": "https://github.com/foo", + "username": "foo" }, "readme_path": "/api/v1/crates/foo_show/0.5.0/readme", "repository": null, diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show_all_yanked.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show_all_yanked.snap index 30269ad6920..b20d9bf3bf0 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show_all_yanked.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show_all_yanked.snap @@ -1,6 +1,7 @@ --- source: src/tests/routes/crates/read.rs expression: response.json() +snapshot_kind: text --- { "categories": [], @@ -78,7 +79,8 @@ expression: response.json() "id": 1, "login": "foo", "name": null, - "url": "https://github.com/foo" + "url": "https://github.com/foo", + "username": "foo" }, "readme_path": "/api/v1/crates/foo_show/0.5.0/readme", "repository": null, @@ -116,7 +118,8 @@ expression: response.json() "id": 1, "login": "foo", "name": null, - "url": "https://github.com/foo" + "url": "https://github.com/foo", + "username": "foo" }, "readme_path": "/api/v1/crates/foo_show/1.0.0/readme", "repository": null, diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__prerelease_versions_not_included_in_reverse_dependencies.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__prerelease_versions_not_included_in_reverse_dependencies.snap index 27e4773acf2..0b71ee66752 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__prerelease_versions_not_included_in_reverse_dependencies.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__prerelease_versions_not_included_in_reverse_dependencies.snap @@ -1,6 +1,7 @@ --- source: src/tests/routes/crates/reverse_dependencies.rs expression: response.json() +snapshot_kind: text --- { "dependencies": [ @@ -50,7 +51,8 @@ expression: response.json() "id": 1, "login": "foo", "name": null, - "url": "https://github.com/foo" + "url": "https://github.com/foo", + "username": "foo" }, "readme_path": "/api/v1/crates/c3/1.0.0/readme", "repository": null, diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies.snap index 7ca7deae51e..7e06a811058 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies.snap @@ -1,6 +1,7 @@ --- source: src/tests/routes/crates/reverse_dependencies.rs expression: response.json() +snapshot_kind: text --- { "dependencies": [ @@ -50,7 +51,8 @@ expression: response.json() "id": 1, "login": "foo", "name": null, - "url": "https://github.com/foo" + "url": "https://github.com/foo", + "username": "foo" }, "readme_path": "/api/v1/crates/c2/1.1.0/readme", "repository": null, diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_includes_published_by_user_when_present.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_includes_published_by_user_when_present.snap index 94dde205c95..dd548de43c3 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_includes_published_by_user_when_present.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_includes_published_by_user_when_present.snap @@ -1,6 +1,7 @@ --- source: src/tests/routes/crates/reverse_dependencies.rs expression: response.json() +snapshot_kind: text --- { "dependencies": [ @@ -62,7 +63,8 @@ expression: response.json() "id": 1, "login": "foo", "name": null, - "url": "https://github.com/foo" + "url": "https://github.com/foo", + "username": "foo" }, "readme_path": "/api/v1/crates/c3/3.0.0/readme", "repository": null, diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_query_supports_u64_version_number_parts.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_query_supports_u64_version_number_parts.snap index 6850cb4856b..8c363689879 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_query_supports_u64_version_number_parts.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_query_supports_u64_version_number_parts.snap @@ -1,6 +1,7 @@ --- source: src/tests/routes/crates/reverse_dependencies.rs expression: response.json() +snapshot_kind: text --- { "dependencies": [ @@ -50,7 +51,8 @@ expression: response.json() "id": 1, "login": "foo", "name": null, - "url": "https://github.com/foo" + "url": "https://github.com/foo", + "username": "foo" }, "readme_path": "/api/v1/crates/c2/1.0.18446744073709551615/readme", "repository": null, diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_when_old_version_doesnt_depend_but_new_does.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_when_old_version_doesnt_depend_but_new_does.snap index 6e17a2fbc8f..ab9cafea9d9 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_when_old_version_doesnt_depend_but_new_does.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_when_old_version_doesnt_depend_but_new_does.snap @@ -1,6 +1,7 @@ --- source: src/tests/routes/crates/reverse_dependencies.rs expression: response.json() +snapshot_kind: text --- { "dependencies": [ @@ -50,7 +51,8 @@ expression: response.json() "id": 1, "login": "foo", "name": null, - "url": "https://github.com/foo" + "url": "https://github.com/foo", + "username": "foo" }, "readme_path": "/api/v1/crates/c2/2.0.0/readme", "repository": null, diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__yanked_versions_not_included_in_reverse_dependencies.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__yanked_versions_not_included_in_reverse_dependencies.snap index 6e17a2fbc8f..ab9cafea9d9 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__yanked_versions_not_included_in_reverse_dependencies.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__yanked_versions_not_included_in_reverse_dependencies.snap @@ -1,6 +1,7 @@ --- source: src/tests/routes/crates/reverse_dependencies.rs expression: response.json() +snapshot_kind: text --- { "dependencies": [ @@ -50,7 +51,8 @@ expression: response.json() "id": 1, "login": "foo", "name": null, - "url": "https://github.com/foo" + "url": "https://github.com/foo", + "username": "foo" }, "readme_path": "/api/v1/crates/c2/2.0.0/readme", "repository": null, diff --git a/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__list__versions.snap b/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__list__versions.snap index d0546ef9c91..6a78599d2d5 100644 --- a/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__list__versions.snap +++ b/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__list__versions.snap @@ -1,6 +1,7 @@ --- source: src/tests/routes/crates/versions/list.rs expression: response.json() +snapshot_kind: text --- { "meta": { @@ -69,7 +70,8 @@ expression: response.json() "id": 1, "login": "foo", "name": null, - "url": "https://github.com/foo" + "url": "https://github.com/foo", + "username": "foo" }, "readme_path": "/api/v1/crates/foo_versions/0.5.1/readme", "repository": null, @@ -107,7 +109,8 @@ expression: response.json() "id": 1, "login": "foo", "name": null, - "url": "https://github.com/foo" + "url": "https://github.com/foo", + "username": "foo" }, "readme_path": "/api/v1/crates/foo_versions/0.5.0/readme", "repository": null, diff --git a/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__read__show_by_crate_name_and_version.snap b/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__read__show_by_crate_name_and_version.snap index 6940293e5ca..96f047cf709 100644 --- a/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__read__show_by_crate_name_and_version.snap +++ b/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__read__show_by_crate_name_and_version.snap @@ -1,6 +1,7 @@ --- source: src/tests/routes/crates/versions/read.rs expression: json +snapshot_kind: text --- { "version": { @@ -32,7 +33,8 @@ expression: json "id": "[id]", "login": "foo", "name": null, - "url": "https://github.com/foo" + "url": "https://github.com/foo", + "username": "foo" }, "readme_path": "/api/v1/crates/foo_vers_show/2.0.0/readme", "repository": null, diff --git a/src/tests/routes/me/snapshots/crates_io__tests__routes__me__get__me-2.snap b/src/tests/routes/me/snapshots/crates_io__tests__routes__me__get__me-2.snap index 5564b16de5e..0275f95e5ff 100644 --- a/src/tests/routes/me/snapshots/crates_io__tests__routes__me__get__me-2.snap +++ b/src/tests/routes/me/snapshots/crates_io__tests__routes__me__get__me-2.snap @@ -1,6 +1,7 @@ --- source: src/tests/routes/me/get.rs expression: response.json() +snapshot_kind: text --- { "owned_crates": [], @@ -14,6 +15,7 @@ expression: response.json() "login": "foo", "name": null, "publish_notifications": true, - "url": "https://github.com/foo" + "url": "https://github.com/foo", + "username": "foo" } } diff --git a/src/tests/routes/me/snapshots/crates_io__tests__routes__me__get__me-3.snap b/src/tests/routes/me/snapshots/crates_io__tests__routes__me__get__me-3.snap index b0ffc3e7fc8..5d511698d04 100644 --- a/src/tests/routes/me/snapshots/crates_io__tests__routes__me__get__me-3.snap +++ b/src/tests/routes/me/snapshots/crates_io__tests__routes__me__get__me-3.snap @@ -1,6 +1,7 @@ --- source: src/tests/routes/me/get.rs expression: response.json() +snapshot_kind: text --- { "owned_crates": [ @@ -20,6 +21,7 @@ expression: response.json() "login": "foo", "name": null, "publish_notifications": true, - "url": "https://github.com/foo" + "url": "https://github.com/foo", + "username": "foo" } } diff --git a/src/tests/routes/me/updates.rs b/src/tests/routes/me/updates.rs index f81f56e525f..8f15a33467b 100644 --- a/src/tests/routes/me/updates.rs +++ b/src/tests/routes/me/updates.rs @@ -78,8 +78,8 @@ async fn following() { .find(|v| v.krate == "bar_fighters") .unwrap(); assert_eq!( - bar_version.published_by.as_ref().unwrap().login, - user_model.gh_login + &bar_version.published_by.as_ref().unwrap().username, + user_model.username.as_ref().unwrap() ); let r: R = user diff --git a/src/tests/routes/users/read.rs b/src/tests/routes/users/read.rs index cd697fc53e7..170aec9906b 100644 --- a/src/tests/routes/users/read.rs +++ b/src/tests/routes/users/read.rs @@ -38,6 +38,7 @@ async fn show_latest_user_case_insensitively() { let user1 = NewUser::builder() .gh_id(1) .gh_login("foobar") + .username("foobar") .name("I was first then deleted my github account") .gh_access_token("bar") .build(); @@ -45,6 +46,7 @@ async fn show_latest_user_case_insensitively() { let user2 = NewUser::builder() .gh_id(2) .gh_login("FOOBAR") + .username("FOOBAR") .name("I was second, I took the foobar username on github") .gh_access_token("bar") .build(); diff --git a/src/tests/user.rs b/src/tests/user.rs index 6e9cb690229..e3b662ac036 100644 --- a/src/tests/user.rs +++ b/src/tests/user.rs @@ -46,6 +46,7 @@ async fn updating_existing_user_doesnt_change_api_token() -> anyhow::Result<()> let user = assert_ok!(User::find(&mut conn, api_token.user_id).await); assert_eq!(user.gh_login, "bar"); + assert_eq!(user.username.unwrap(), "bar"); assert_eq!(user.gh_access_token.expose_secret(), "bar_token"); Ok(()) diff --git a/src/typosquat/test_util.rs b/src/typosquat/test_util.rs index 69875e1261b..ede066dfdf7 100644 --- a/src/typosquat/test_util.rs +++ b/src/typosquat/test_util.rs @@ -41,6 +41,7 @@ pub mod faker { NewUser::builder() .gh_id(next_gh_id()) .gh_login(login) + .username(login) .gh_access_token("token") .build() .insert(conn) diff --git a/src/views.rs b/src/views.rs index c3b3be8443e..1f3089cc16c 100644 --- a/src/views.rs +++ b/src/views.rs @@ -521,10 +521,16 @@ pub struct EncodableOwner { #[schema(example = 42)] pub id: i32, - /// The login name of the team or user. + // `login` and `username` should contain the same value for now. + // `login` is deprecated; can be removed when all frontends have migrated to `username`. + /// The GitHub login of the team or user. #[schema(example = "ghost")] pub login: String, + /// The crates.io username of the team or user. + #[schema(example = "ghost")] + pub username: String, + /// The kind of the owner (`user` or `team`). #[schema(example = "user")] pub kind: String, @@ -548,14 +554,17 @@ impl From for EncodableOwner { Owner::User(User { id, name, + username, gh_login, gh_avatar, .. }) => { let url = format!("https://github.com/{gh_login}"); + let username = username.unwrap_or(gh_login); Self { id, - login: gh_login, + login: username.clone(), + username, avatar: gh_avatar, url: Some(url), name, @@ -572,7 +581,8 @@ impl From for EncodableOwner { let url = github::team_url(&login); Self { id, - login, + login: login.clone(), + username: login, url: Some(url), avatar, name, @@ -672,10 +682,16 @@ pub struct EncodablePrivateUser { #[schema(example = 42)] pub id: i32, - /// The user's login name. + // `login` and `username` should contain the same value for now. + // `login` is deprecated; can be removed when all frontends have migrated to `username`. + /// The user's GitHub login. #[schema(example = "ghost")] pub login: String, + /// The user's crates.io username. + #[schema(example = "ghost")] + pub username: String, + /// Whether the user's email address has been verified. #[schema(example = true)] pub email_verified: bool, @@ -720,6 +736,7 @@ impl EncodablePrivateUser { let User { id, name, + username, gh_login, gh_avatar, is_admin, @@ -727,14 +744,16 @@ impl EncodablePrivateUser { .. } = user; let url = format!("https://github.com/{gh_login}"); + let username = username.unwrap_or(gh_login); EncodablePrivateUser { id, + login: username.clone(), + username, email, email_verified, email_verification_sent, avatar: gh_avatar, - login: gh_login, name, url: Some(url), is_admin, @@ -750,10 +769,16 @@ pub struct EncodablePublicUser { #[schema(example = 42)] pub id: i32, - /// The user's login name. + // `login` and `username` should contain the same value for now. + // `login` is deprecated; can be removed when all frontends have migrated to `username`. + /// The user's GitHub login name. #[schema(example = "ghost")] pub login: String, + /// The user's crates.io username. + #[schema(example = "ghost")] + pub username: String, + /// The user's display name, if set. #[schema(example = "Kate Morgan")] pub name: Option, @@ -773,16 +798,19 @@ impl From for EncodablePublicUser { let User { id, name, + username, gh_login, gh_avatar, .. } = user; let url = format!("https://github.com/{gh_login}"); + let username = username.unwrap_or(gh_login); EncodablePublicUser { id, - avatar: gh_avatar, - login: gh_login, + login: username.clone(), + username, name, + avatar: gh_avatar, url, } } @@ -1108,6 +1136,7 @@ mod tests { user: EncodablePublicUser { id: 0, login: String::new(), + username: String::new(), name: None, avatar: None, url: String::new(), diff --git a/src/worker/jobs/downloads/update_metadata.rs b/src/worker/jobs/downloads/update_metadata.rs index a2d676c594c..5cc20a90c64 100644 --- a/src/worker/jobs/downloads/update_metadata.rs +++ b/src/worker/jobs/downloads/update_metadata.rs @@ -115,6 +115,7 @@ mod tests { NewUser::builder() .gh_id(2) .gh_login("login") + .username("login") .gh_access_token("access_token") .build() .insert(conn) diff --git a/src/worker/jobs/expiry_notification.rs b/src/worker/jobs/expiry_notification.rs index 4119eab29aa..1c4add805ae 100644 --- a/src/worker/jobs/expiry_notification.rs +++ b/src/worker/jobs/expiry_notification.rs @@ -186,6 +186,7 @@ mod tests { let user = NewUser::builder() .gh_id(0) .gh_login("a") + .username("a") .gh_access_token("token") .build() .insert(&mut conn) From 49124ba8c1f0b05acf39b826c75b76f768e61163 Mon Sep 17 00:00:00 2001 From: "Carol (Nichols || Goulding)" Date: Tue, 4 Mar 2025 20:26:20 -0500 Subject: [PATCH 4/6] (untested) SQL to backfill username (deploy 4) --- script/backfill-usernames.sql | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 script/backfill-usernames.sql diff --git a/script/backfill-usernames.sql b/script/backfill-usernames.sql new file mode 100644 index 00000000000..59372812ce3 --- /dev/null +++ b/script/backfill-usernames.sql @@ -0,0 +1,2 @@ +UPDATE users +SET username = gh_login; From 19125bb81655e5d0daf3d4b079af210401619585 Mon Sep 17 00:00:00 2001 From: "Carol (Nichols || Goulding)" Date: Tue, 4 Mar 2025 14:03:12 -0500 Subject: [PATCH 5/6] Transition views to use username instead of login (deploy 5) --- app/components/owners-list.hbs | 6 ++--- app/components/pending-owner-invite-row.hbs | 4 ++-- app/components/user-avatar.js | 4 ++-- app/components/user-link.hbs | 2 +- app/components/version-list/row.hbs | 4 ++-- app/controllers/crate/settings.js | 6 ++--- app/models/team.js | 7 +++--- app/models/user.js | 1 + app/services/session.js | 2 +- app/templates/crate/settings.hbs | 14 ++++++------ app/templates/settings/profile.hbs | 2 +- app/templates/user.hbs | 2 +- e2e/acceptance/api-token.spec.ts | 1 + e2e/acceptance/crate.spec.ts | 6 ++--- e2e/acceptance/dashboard.spec.ts | 1 + e2e/acceptance/login.spec.ts | 1 + e2e/acceptance/settings/add-owner.spec.ts | 2 +- e2e/acceptance/settings/remove-owner.spec.ts | 6 ++--- e2e/acceptance/sudo.spec.ts | 1 + e2e/routes/settings/tokens/index.spec.ts | 1 + e2e/routes/settings/tokens/new.spec.ts | 1 + packages/crates-io-msw/fixtures/teams.js | 2 ++ packages/crates-io-msw/fixtures/users.js | 3 +++ .../handlers/crates/add-owners.js | 16 +++++++------- .../handlers/crates/add-owners.test.js | 6 ++--- .../handlers/crates/remove-owners.js | 4 +++- .../handlers/crates/remove-owners.test.js | 6 ++--- .../handlers/crates/team-owners.test.js | 1 + .../handlers/crates/user-owners.test.js | 1 + .../handlers/invites/legacy-list.test.js | 3 +++ .../handlers/invites/list.test.js | 5 +++++ packages/crates-io-msw/handlers/teams/get.js | 4 ++-- .../crates-io-msw/handlers/teams/get.test.js | 3 ++- packages/crates-io-msw/handlers/users/get.js | 4 ++-- .../crates-io-msw/handlers/users/get.test.js | 3 ++- .../crates-io-msw/handlers/users/me.test.js | 1 + .../handlers/versions/list.test.js | 1 + .../crates-io-msw/models/api-token.test.js | 1 + .../models/crate-owner-invitation.test.js | 2 ++ .../models/crate-ownership.test.js | 2 ++ .../crates-io-msw/models/msw-session.test.js | 1 + packages/crates-io-msw/models/team.js | 2 ++ packages/crates-io-msw/models/team.test.js | 2 ++ packages/crates-io-msw/models/user.js | 4 +++- packages/crates-io-msw/models/user.test.js | 2 ++ src/controllers/krate/owners.rs | 4 ++-- src/tests/issues/issue1205.rs | 2 +- src/tests/routes/crates/owners/add.rs | 4 ++-- src/tests/routes/crates/owners/remove.rs | 4 ++-- src/tests/routes/users/read.rs | 2 ++ src/tests/team.rs | 2 ++ tests/acceptance/api-token-test.js | 1 + tests/acceptance/crate-test.js | 6 ++--- tests/acceptance/dashboard-test.js | 1 + tests/acceptance/login-test.js | 1 + tests/acceptance/settings/add-owner-test.js | 2 +- .../acceptance/settings/remove-owner-test.js | 6 ++--- tests/acceptance/sudo-test.js | 1 + tests/components/owners-list-test.js | 22 +++++++++---------- tests/models/crate-test.js | 6 ++--- tests/routes/settings/tokens/index-test.js | 1 + tests/routes/settings/tokens/new-test.js | 1 + 62 files changed, 136 insertions(+), 83 deletions(-) diff --git a/app/components/owners-list.hbs b/app/components/owners-list.hbs index a4eac17b56b..c70170064f9 100644 --- a/app/components/owners-list.hbs +++ b/app/components/owners-list.hbs @@ -7,12 +7,12 @@
  • {{/each}} diff --git a/app/components/pending-owner-invite-row.hbs b/app/components/pending-owner-invite-row.hbs index bad2d9e790b..01992cf1cf7 100644 --- a/app/components/pending-owner-invite-row.hbs +++ b/app/components/pending-owner-invite-row.hbs @@ -19,8 +19,8 @@
    Invited by: - - {{@invite.inviter.login}} + + {{@invite.inviter.username}}
    diff --git a/app/components/user-avatar.js b/app/components/user-avatar.js index 37012f4738d..c65ba3daf0c 100644 --- a/app/components/user-avatar.js +++ b/app/components/user-avatar.js @@ -13,8 +13,8 @@ export default class UserAvatar extends Component { get alt() { return this.args.user.name === null - ? `(${this.args.user.login})` - : `${this.args.user.name} (${this.args.user.login})`; + ? `(${this.args.user.username})` + : `${this.args.user.name} (${this.args.user.username})`; } get title() { diff --git a/app/components/user-link.hbs b/app/components/user-link.hbs index 13f128379cc..58865fd2dd1 100644 --- a/app/components/user-link.hbs +++ b/app/components/user-link.hbs @@ -1 +1 @@ -{{yield}} \ No newline at end of file +{{yield}} \ No newline at end of file diff --git a/app/components/version-list/row.hbs b/app/components/version-list/row.hbs index e6630036423..c1eaead5a18 100644 --- a/app/components/version-list/row.hbs +++ b/app/components/version-list/row.hbs @@ -40,9 +40,9 @@ {{#if @version.published_by}} by - + - {{or @version.published_by.name @version.published_by.login}} + {{or @version.published_by.name @version.published_by.username}} {{/if}} diff --git a/app/controllers/crate/settings.js b/app/controllers/crate/settings.js index 72d38bdce9a..e55bb789cdd 100644 --- a/app/controllers/crate/settings.js +++ b/app/controllers/crate/settings.js @@ -32,19 +32,19 @@ export default class CrateSettingsController extends Controller { removeOwnerTask = task(async owner => { try { - await this.crate.removeOwner(owner.get('login')); + await this.crate.removeOwner(owner.get('username')); if (owner.kind === 'team') { this.notifications.success(`Team ${owner.get('display_name')} removed as crate owner`); let owner_team = await this.crate.owner_team; removeOwner(owner_team, owner); } else { - this.notifications.success(`User ${owner.get('login')} removed as crate owner`); + this.notifications.success(`User ${owner.get('username')} removed as crate owner`); let owner_user = await this.crate.owner_user; removeOwner(owner_user, owner); } } catch (error) { - let subject = owner.kind === 'team' ? `team ${owner.get('display_name')}` : `user ${owner.get('login')}`; + let subject = owner.kind === 'team' ? `team ${owner.get('display_name')}` : `user ${owner.get('username')}`; let message = `Failed to remove the ${subject} as crate owner`; let detail = error.errors?.[0]?.detail; diff --git a/app/models/team.js b/app/models/team.js index 2d69e1a108e..07104f40661 100644 --- a/app/models/team.js +++ b/app/models/team.js @@ -4,15 +4,16 @@ export default class Team extends Model { @attr email; @attr name; @attr login; + @attr username; @attr api_token; @attr avatar; @attr url; @attr kind; get org_name() { - let login = this.login; - let login_split = login.split(':'); - return login_split[1]; + let username = this.username; + let username_split = username.split(':'); + return username_split[1]; } get display_name() { diff --git a/app/models/user.js b/app/models/user.js index 9e96f47adcf..22dbff658e0 100644 --- a/app/models/user.js +++ b/app/models/user.js @@ -13,6 +13,7 @@ export default class User extends Model { @attr name; @attr is_admin; @attr login; + @attr username; @attr avatar; @attr url; @attr kind; diff --git a/app/services/session.js b/app/services/session.js index baf5a0af7bb..37f66289039 100644 --- a/app/services/session.js +++ b/app/services/session.js @@ -194,7 +194,7 @@ export default class SessionService extends Service { } let currentUser = this.store.push(this.store.normalize('user', response.user)); - debug(`User found: ${currentUser.login}`); + debug(`User found: ${currentUser.username}`); let ownedCrates = response.owned_crates.map(c => this.store.push(this.store.normalize('owned-crate', c))); let { id } = currentUser; diff --git a/app/templates/crate/settings.hbs b/app/templates/crate/settings.hbs index 5487e88876c..4d6be016528 100644 --- a/app/templates/crate/settings.hbs +++ b/app/templates/crate/settings.hbs @@ -18,11 +18,11 @@
    {{#each this.crate.owner_team as |team|}} -
    - +
    + - + {{team.display_name}}
    @@ -32,15 +32,15 @@
    {{/each}} {{#each this.crate.owner_user as |user|}} -
    - +
    + - + {{#if user.name}} {{user.name}} {{else}} - {{user.login}} + {{user.username}} {{/if}}
    diff --git a/app/templates/settings/profile.hbs b/app/templates/settings/profile.hbs index c4ab8eb42e1..16f3ca2e1b3 100644 --- a/app/templates/settings/profile.hbs +++ b/app/templates/settings/profile.hbs @@ -13,7 +13,7 @@
    Name
    {{ this.model.user.name }}
    GitHub Account
    -
    {{ this.model.user.login }}
    +
    {{ this.model.user.username }}
    diff --git a/app/templates/user.hbs b/app/templates/user.hbs index 10b8c202970..c70ae7d95aa 100644 --- a/app/templates/user.hbs +++ b/app/templates/user.hbs @@ -1,7 +1,7 @@

    - {{ this.model.user.login }} + {{ this.model.user.username }}

    {{svg-jar "github" alt="GitHub profile"}} diff --git a/e2e/acceptance/api-token.spec.ts b/e2e/acceptance/api-token.spec.ts index c7a3f2827df..c89819e76c4 100644 --- a/e2e/acceptance/api-token.spec.ts +++ b/e2e/acceptance/api-token.spec.ts @@ -5,6 +5,7 @@ test.describe('Acceptance | api-tokens', { tag: '@acceptance' }, () => { test.beforeEach(async ({ msw }) => { let user = msw.db.user.create({ login: 'johnnydee', + username: 'johnnydee', name: 'John Doe', email: 'john@doe.com', avatar: 'https://avatars2.githubusercontent.com/u/1234567?v=4', diff --git a/e2e/acceptance/crate.spec.ts b/e2e/acceptance/crate.spec.ts index bf09fe3628b..d7a77404742 100644 --- a/e2e/acceptance/crate.spec.ts +++ b/e2e/acceptance/crate.spec.ts @@ -213,7 +213,7 @@ test.describe('Acceptance | crate page', { tag: '@acceptance' }, () => { test.skip('crates can be yanked by owner', async ({ page, msw }) => { loadFixtures(msw.db); - let user = msw.db.user.findFirst({ where: { login: { equals: 'thehydroimpulse' } } }); + let user = msw.db.user.findFirst({ where: { username: { equals: 'thehydroimpulse' } } }); await msw.authenticateAs(user); await page.goto('/crates/nanomsg/0.5.0'); @@ -241,7 +241,7 @@ test.describe('Acceptance | crate page', { tag: '@acceptance' }, () => { test('navigating to the owners page when not an owner', async ({ page, msw }) => { loadFixtures(msw.db); - let user = msw.db.user.findFirst({ where: { login: { equals: 'iain8' } } }); + let user = msw.db.user.findFirst({ where: { username: { equals: 'iain8' } } }); await msw.authenticateAs(user); await page.goto('/crates/nanomsg'); @@ -252,7 +252,7 @@ test.describe('Acceptance | crate page', { tag: '@acceptance' }, () => { test('navigating to the settings page', async ({ page, msw }) => { loadFixtures(msw.db); - let user = msw.db.user.findFirst({ where: { login: { equals: 'thehydroimpulse' } } }); + let user = msw.db.user.findFirst({ where: { username: { equals: 'thehydroimpulse' } } }); await msw.authenticateAs(user); await page.goto('/crates/nanomsg'); diff --git a/e2e/acceptance/dashboard.spec.ts b/e2e/acceptance/dashboard.spec.ts index 1baefd56928..1ce2bffe78f 100644 --- a/e2e/acceptance/dashboard.spec.ts +++ b/e2e/acceptance/dashboard.spec.ts @@ -12,6 +12,7 @@ test.describe('Acceptance | Dashboard', { tag: '@acceptance' }, () => { test('shows the dashboard when logged in', async ({ page, msw, percy }) => { let user = msw.db.user.create({ login: 'johnnydee', + username: 'johnnydee', name: 'John Doe', email: 'john@doe.com', avatar: 'https://avatars2.githubusercontent.com/u/1234567?v=4', diff --git a/e2e/acceptance/login.spec.ts b/e2e/acceptance/login.spec.ts index aaeb9704747..ab9a9bc2b41 100644 --- a/e2e/acceptance/login.spec.ts +++ b/e2e/acceptance/login.spec.ts @@ -28,6 +28,7 @@ test.describe('Acceptance | Login', { tag: '@acceptance' }, () => { user: { id: 42, login: 'johnnydee', + username: 'johnnydee', name: 'John Doe', email: 'john@doe.name', avatar: 'https://avatars2.githubusercontent.com/u/12345?v=4', diff --git a/e2e/acceptance/settings/add-owner.spec.ts b/e2e/acceptance/settings/add-owner.spec.ts index 43f53cbdf0f..0368096bb10 100644 --- a/e2e/acceptance/settings/add-owner.spec.ts +++ b/e2e/acceptance/settings/add-owner.spec.ts @@ -29,7 +29,7 @@ test.describe('Acceptance | Settings | Add Owner', { tag: '@acceptance' }, () => await page.click('[data-test-save-button]'); await expect(page.locator('[data-test-notification-message="error"]')).toHaveText( - 'Error sending invite: could not find user with login `spookyghostboo`', + 'Error sending invite: could not find user with username `spookyghostboo`', ); await expect(page.locator('[data-test-owners] [data-test-owner-team]')).toHaveCount(2); await expect(page.locator('[data-test-owners] [data-test-owner-user]')).toHaveCount(2); diff --git a/e2e/acceptance/settings/remove-owner.spec.ts b/e2e/acceptance/settings/remove-owner.spec.ts index f767ed711c4..093ad53b7fc 100644 --- a/e2e/acceptance/settings/remove-owner.spec.ts +++ b/e2e/acceptance/settings/remove-owner.spec.ts @@ -37,10 +37,10 @@ test.describe('Acceptance | Settings | Remove Owner', { tag: '@acceptance' }, () await msw.worker.use(http.delete('/api/v1/crates/nanomsg/owners', () => error)); await page.goto(`/crates/${crate.name}/settings`); - await page.click(`[data-test-owner-user="${user2.login}"] [data-test-remove-owner-button]`); + await page.click(`[data-test-owner-user="${user2.username}"] [data-test-remove-owner-button]`); await expect(page.locator('[data-test-notification-message="error"]')).toHaveText( - `Failed to remove the user ${user2.login} as crate owner: nope`, + `Failed to remove the user ${user2.username} as crate owner: nope`, ); await expect(page.locator('[data-test-owner-user]')).toHaveCount(2); }); @@ -62,7 +62,7 @@ test.describe('Acceptance | Settings | Remove Owner', { tag: '@acceptance' }, () await msw.worker.use(http.delete('/api/v1/crates/nanomsg/owners', () => error)); await page.goto(`/crates/${crate.name}/settings`); - await page.click(`[data-test-owner-team="${team1.login}"] [data-test-remove-owner-button]`); + await page.click(`[data-test-owner-team="${team1.username}"] [data-test-remove-owner-button]`); await expect(page.locator('[data-test-notification-message="error"]')).toHaveText( `Failed to remove the team ${team1.org}/${team1.name} as crate owner: nope`, diff --git a/e2e/acceptance/sudo.spec.ts b/e2e/acceptance/sudo.spec.ts index 3970bbd60da..23a0439ff58 100644 --- a/e2e/acceptance/sudo.spec.ts +++ b/e2e/acceptance/sudo.spec.ts @@ -5,6 +5,7 @@ test.describe('Acceptance | sudo', { tag: '@acceptance' }, () => { async function prepare(msw, { isAdmin = false } = {}) { let user = msw.db.user.create({ login: 'johnnydee', + username: 'johnnydee', name: 'John Doe', email: 'john@doe.com', avatar: 'https://avatars2.githubusercontent.com/u/1234567?v=4', diff --git a/e2e/routes/settings/tokens/index.spec.ts b/e2e/routes/settings/tokens/index.spec.ts index 63661616294..1bb42528f50 100644 --- a/e2e/routes/settings/tokens/index.spec.ts +++ b/e2e/routes/settings/tokens/index.spec.ts @@ -4,6 +4,7 @@ test.describe('/settings/tokens', { tag: '@routes' }, () => { test('reloads all tokens from the server', async ({ page, msw }) => { let user = msw.db.user.create({ login: 'johnnydee', + username: 'johnnydee', name: 'John Doe', email: 'john@doe.com', avatar: 'https://avatars2.githubusercontent.com/u/1234567?v=4', diff --git a/e2e/routes/settings/tokens/new.spec.ts b/e2e/routes/settings/tokens/new.spec.ts index 87b1a8bfb8e..be46a7513cc 100644 --- a/e2e/routes/settings/tokens/new.spec.ts +++ b/e2e/routes/settings/tokens/new.spec.ts @@ -6,6 +6,7 @@ test.describe('/settings/tokens/new', { tag: '@routes' }, () => { async function prepare(msw) { let user = msw.db.user.create({ login: 'johnnydee', + username: 'johnnydee', name: 'John Doe', email: 'john@doe.com', avatar: 'https://avatars2.githubusercontent.com/u/1234567?v=4', diff --git a/packages/crates-io-msw/fixtures/teams.js b/packages/crates-io-msw/fixtures/teams.js index 0c753788449..dbd2ca25462 100644 --- a/packages/crates-io-msw/fixtures/teams.js +++ b/packages/crates-io-msw/fixtures/teams.js @@ -3,6 +3,7 @@ export default [ avatar: 'https://avatars.githubusercontent.com/u/565790?v=3', id: 1, login: 'github:org:thehydroimpulse', + username: 'github:org:thehydroimpulse', name: 'thehydroimpulseteam', url: 'https://github.com/org_test', }, @@ -10,6 +11,7 @@ export default [ avatar: 'https://avatars.githubusercontent.com/u/9447137?v=3', id: 303, login: 'github:org:blabaere', + username: 'github:org:blabaere', name: 'Team Benoît Labaere', url: 'https://github.com/blabaere', }, diff --git a/packages/crates-io-msw/fixtures/users.js b/packages/crates-io-msw/fixtures/users.js index 00243bdce19..d6628f4b192 100644 --- a/packages/crates-io-msw/fixtures/users.js +++ b/packages/crates-io-msw/fixtures/users.js @@ -4,6 +4,7 @@ export default [ email: null, id: 303, login: 'blabaere', + username: 'blabaere', name: 'Benoît Labaere', url: 'https://github.com/blabaere', }, @@ -12,6 +13,7 @@ export default [ email: 'dnfagnan@gmail.com', id: 2, login: 'thehydroimpulse', + username: 'thehydroimpulse', name: 'Daniel Fagnan', url: 'https://github.com/thehydroimpulse', }, @@ -20,6 +22,7 @@ export default [ email: 'iain@fastmail.com', id: 10_982, login: 'iain8', + username: 'iain8', name: 'Iain Buchanan', url: 'https://github.com/iain8', }, diff --git a/packages/crates-io-msw/handlers/crates/add-owners.js b/packages/crates-io-msw/handlers/crates/add-owners.js index 227d27ef6a8..77f9bcdda4d 100644 --- a/packages/crates-io-msw/handlers/crates/add-owners.js +++ b/packages/crates-io-msw/handlers/crates/add-owners.js @@ -20,25 +20,25 @@ export default http.put('/api/v1/crates/:name/owners', async ({ request, params let users = []; let teams = []; let msgs = []; - for (let login of body.owners) { - if (login.includes(':')) { - let team = db.team.findFirst({ where: { login: { equals: login } } }); + for (let username of body.owners) { + if (username.includes(':')) { + let team = db.team.findFirst({ where: { username: { equals: username } } }); if (!team) { - let errorMessage = `could not find team with login \`${login}\``; + let errorMessage = `could not find team with username \`${username}\``; return HttpResponse.json({ errors: [{ detail: errorMessage }] }, { status: 404 }); } teams.push(team); - msgs.push(`team ${login} has been added as an owner of crate ${crate.name}`); + msgs.push(`team ${username} has been added as an owner of crate ${crate.name}`); } else { - let user = db.user.findFirst({ where: { login: { equals: login } } }); + let user = db.user.findFirst({ where: { username: { equals: username } } }); if (!user) { - let errorMessage = `could not find user with login \`${login}\``; + let errorMessage = `could not find user with username \`${username}\``; return HttpResponse.json({ errors: [{ detail: errorMessage }] }, { status: 404 }); } users.push(user); - msgs.push(`user ${login} has been invited to be an owner of crate ${crate.name}`); + msgs.push(`user ${username} has been invited to be an owner of crate ${crate.name}`); } } diff --git a/packages/crates-io-msw/handlers/crates/add-owners.test.js b/packages/crates-io-msw/handlers/crates/add-owners.test.js index cdaae3e7ae6..e936b6402df 100644 --- a/packages/crates-io-msw/handlers/crates/add-owners.test.js +++ b/packages/crates-io-msw/handlers/crates/add-owners.test.js @@ -30,7 +30,7 @@ test('can add new owner', async function () { let user2 = db.user.create(); - let body = JSON.stringify({ owners: [user2.login] }); + let body = JSON.stringify({ owners: [user2.username] }); let response = await fetch('/api/v1/crates/foo/owners', { method: 'PUT', body }); assert.strictEqual(response.status, 200); assert.deepEqual(await response.json(), { @@ -57,7 +57,7 @@ test('can add team owner', async function () { let team = db.team.create(); - let body = JSON.stringify({ owners: [team.login] }); + let body = JSON.stringify({ owners: [team.username] }); let response = await fetch('/api/v1/crates/foo/owners', { method: 'PUT', body }); assert.strictEqual(response.status, 200); assert.deepEqual(await response.json(), { @@ -87,7 +87,7 @@ test('can add multiple owners', async function () { let user2 = db.user.create(); let user3 = db.user.create(); - let body = JSON.stringify({ owners: [user2.login, team.login, user3.login] }); + let body = JSON.stringify({ owners: [user2.username, team.username, user3.username] }); let response = await fetch('/api/v1/crates/foo/owners', { method: 'PUT', body }); assert.strictEqual(response.status, 200); assert.deepEqual(await response.json(), { diff --git a/packages/crates-io-msw/handlers/crates/remove-owners.js b/packages/crates-io-msw/handlers/crates/remove-owners.js index a221967d4fb..2a9d8f9c7b2 100644 --- a/packages/crates-io-msw/handlers/crates/remove-owners.js +++ b/packages/crates-io-msw/handlers/crates/remove-owners.js @@ -19,7 +19,9 @@ export default http.delete('/api/v1/crates/:name/owners', async ({ request, para for (let owner of body.owners) { let ownership = db.crateOwnership.findFirst({ - where: owner.includes(':') ? { team: { login: { equals: owner } } } : { user: { login: { equals: owner } } }, + where: owner.includes(':') + ? { team: { username: { equals: owner } } } + : { user: { username: { equals: owner } } }, }); if (!ownership) return notFound(); db.crateOwnership.delete({ where: { id: { equals: ownership.id } } }); diff --git a/packages/crates-io-msw/handlers/crates/remove-owners.test.js b/packages/crates-io-msw/handlers/crates/remove-owners.test.js index d1c84998227..ac5811aef76 100644 --- a/packages/crates-io-msw/handlers/crates/remove-owners.test.js +++ b/packages/crates-io-msw/handlers/crates/remove-owners.test.js @@ -31,7 +31,7 @@ test('can remove a user owner', async function () { let user2 = db.user.create(); db.crateOwnership.create({ crate, user: user2 }); - let body = JSON.stringify({ owners: [user2.login] }); + let body = JSON.stringify({ owners: [user2.username] }); let response = await fetch('/api/v1/crates/foo/owners', { method: 'DELETE', body }); assert.strictEqual(response.status, 200); assert.deepEqual(await response.json(), { ok: true, msg: 'owners successfully removed' }); @@ -54,7 +54,7 @@ test('can remove a team owner', async function () { let team = db.team.create(); db.crateOwnership.create({ crate, team }); - let body = JSON.stringify({ owners: [team.login] }); + let body = JSON.stringify({ owners: [team.username] }); let response = await fetch('/api/v1/crates/foo/owners', { method: 'DELETE', body }); assert.strictEqual(response.status, 200); assert.deepEqual(await response.json(), { ok: true, msg: 'owners successfully removed' }); @@ -81,7 +81,7 @@ test('can remove multiple owners', async function () { let user2 = db.user.create(); db.crateOwnership.create({ crate, user: user2 }); - let body = JSON.stringify({ owners: [user2.login, team.login] }); + let body = JSON.stringify({ owners: [user2.username, team.username] }); let response = await fetch('/api/v1/crates/foo/owners', { method: 'DELETE', body }); assert.strictEqual(response.status, 200); assert.deepEqual(await response.json(), { ok: true, msg: 'owners successfully removed' }); diff --git a/packages/crates-io-msw/handlers/crates/team-owners.test.js b/packages/crates-io-msw/handlers/crates/team-owners.test.js index 25a9981aff5..ea8cb1570fb 100644 --- a/packages/crates-io-msw/handlers/crates/team-owners.test.js +++ b/packages/crates-io-msw/handlers/crates/team-owners.test.js @@ -32,6 +32,7 @@ test('returns the list of teams that own the specified crate', async function () avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4', kind: 'team', login: 'github:rust-lang:maintainers', + username: 'github:rust-lang:maintainers', name: 'maintainers', url: 'https://github.com/rust-lang', }, diff --git a/packages/crates-io-msw/handlers/crates/user-owners.test.js b/packages/crates-io-msw/handlers/crates/user-owners.test.js index e5426153674..1cbfcc28ab2 100644 --- a/packages/crates-io-msw/handlers/crates/user-owners.test.js +++ b/packages/crates-io-msw/handlers/crates/user-owners.test.js @@ -32,6 +32,7 @@ test('returns the list of users that own the specified crate', async function () avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4', kind: 'user', login: 'john-doe', + username: 'john-doe', name: 'John Doe', url: 'https://github.com/john-doe', }, diff --git a/packages/crates-io-msw/handlers/invites/legacy-list.test.js b/packages/crates-io-msw/handlers/invites/legacy-list.test.js index 91e7b4c768e..80ca10e7d0a 100644 --- a/packages/crates-io-msw/handlers/invites/legacy-list.test.js +++ b/packages/crates-io-msw/handlers/invites/legacy-list.test.js @@ -63,6 +63,7 @@ test('returns the list of invitations for the authenticated user', async functio avatar: user.avatar, id: Number(user.id), login: user.login, + username: user.username, name: user.name, url: user.url, }, @@ -70,6 +71,7 @@ test('returns the list of invitations for the authenticated user', async functio avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4', id: Number(inviter.id), login: 'janed', + username: 'janed', name: 'janed', url: 'https://github.com/janed', }, @@ -77,6 +79,7 @@ test('returns the list of invitations for the authenticated user', async functio avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4', id: Number(inviter2.id), login: 'wycats', + username: 'wycats', name: 'wycats', url: 'https://github.com/wycats', }, diff --git a/packages/crates-io-msw/handlers/invites/list.test.js b/packages/crates-io-msw/handlers/invites/list.test.js index 551bcd05903..9f5df26ecb5 100644 --- a/packages/crates-io-msw/handlers/invites/list.test.js +++ b/packages/crates-io-msw/handlers/invites/list.test.js @@ -56,6 +56,7 @@ test('happy path (invitee_id)', async function () { login: user.login, name: user.name, url: user.url, + username: user.username, }, { avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4', @@ -63,6 +64,7 @@ test('happy path (invitee_id)', async function () { login: 'janed', name: 'janed', url: 'https://github.com/janed', + username: 'janed', }, { avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4', @@ -70,6 +72,7 @@ test('happy path (invitee_id)', async function () { login: 'wycats', name: 'wycats', url: 'https://github.com/wycats', + username: 'wycats', }, ], meta: { @@ -164,6 +167,7 @@ test('happy path (crate_name)', async function () { login: user.login, name: user.name, url: user.url, + username: user.username, }, { avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4', @@ -171,6 +175,7 @@ test('happy path (crate_name)', async function () { login: 'wycats', name: 'wycats', url: 'https://github.com/wycats', + username: 'wycats', }, ], meta: { diff --git a/packages/crates-io-msw/handlers/teams/get.js b/packages/crates-io-msw/handlers/teams/get.js index 8fb593aa11f..8e1a70a71d0 100644 --- a/packages/crates-io-msw/handlers/teams/get.js +++ b/packages/crates-io-msw/handlers/teams/get.js @@ -5,8 +5,8 @@ import { serializeTeam } from '../../serializers/team.js'; import { notFound } from '../../utils/handlers.js'; export default http.get('/api/v1/teams/:team_id', ({ params }) => { - let login = params.team_id; - let team = db.team.findFirst({ where: { login: { equals: login } } }); + let username = params.team_id; + let team = db.team.findFirst({ where: { username: { equals: username } } }); if (!team) { return notFound(); } diff --git a/packages/crates-io-msw/handlers/teams/get.test.js b/packages/crates-io-msw/handlers/teams/get.test.js index 7e164e26f74..35ae9f68d71 100644 --- a/packages/crates-io-msw/handlers/teams/get.test.js +++ b/packages/crates-io-msw/handlers/teams/get.test.js @@ -11,13 +11,14 @@ test('returns 404 for unknown teams', async function () { test('returns a team object for known teams', async function () { let team = db.team.create({ name: 'maintainers' }); - let response = await fetch(`/api/v1/teams/${team.login}`); + let response = await fetch(`/api/v1/teams/${team.username}`); assert.strictEqual(response.status, 200); assert.deepEqual(await response.json(), { team: { id: 1, avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4', login: 'github:rust-lang:maintainers', + username: 'github:rust-lang:maintainers', name: 'maintainers', url: 'https://github.com/rust-lang', }, diff --git a/packages/crates-io-msw/handlers/users/get.js b/packages/crates-io-msw/handlers/users/get.js index ee299d708d1..ef4a68caf7e 100644 --- a/packages/crates-io-msw/handlers/users/get.js +++ b/packages/crates-io-msw/handlers/users/get.js @@ -5,8 +5,8 @@ import { serializeUser } from '../../serializers/user.js'; import { notFound } from '../../utils/handlers.js'; export default http.get('/api/v1/users/:user_id', ({ params }) => { - let login = params.user_id; - let user = db.user.findFirst({ where: { login: { equals: login } } }); + let username = params.user_id; + let user = db.user.findFirst({ where: { username: { equals: username } } }); if (!user) { return notFound(); } diff --git a/packages/crates-io-msw/handlers/users/get.test.js b/packages/crates-io-msw/handlers/users/get.test.js index f5314186740..e5e7476e264 100644 --- a/packages/crates-io-msw/handlers/users/get.test.js +++ b/packages/crates-io-msw/handlers/users/get.test.js @@ -11,13 +11,14 @@ test('returns 404 for unknown users', async function () { test('returns a user object for known users', async function () { let user = db.user.create({ name: 'John Doe' }); - let response = await fetch(`/api/v1/users/${user.login}`); + let response = await fetch(`/api/v1/users/${user.username}`); assert.strictEqual(response.status, 200); assert.deepEqual(await response.json(), { user: { id: 1, avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4', login: 'john-doe', + username: 'john-doe', name: 'John Doe', url: 'https://github.com/john-doe', }, diff --git a/packages/crates-io-msw/handlers/users/me.test.js b/packages/crates-io-msw/handlers/users/me.test.js index fba39926019..f480abf57d7 100644 --- a/packages/crates-io-msw/handlers/users/me.test.js +++ b/packages/crates-io-msw/handlers/users/me.test.js @@ -17,6 +17,7 @@ test('returns the `user` resource including the private fields', async function email_verified: true, is_admin: false, login: 'user-1', + username: 'user-1', name: 'User 1', publish_notifications: true, url: 'https://github.com/user-1', diff --git a/packages/crates-io-msw/handlers/versions/list.test.js b/packages/crates-io-msw/handlers/versions/list.test.js index 263d0f0d007..372e015cbea 100644 --- a/packages/crates-io-msw/handlers/versions/list.test.js +++ b/packages/crates-io-msw/handlers/versions/list.test.js @@ -69,6 +69,7 @@ test('returns all versions belonging to the specified crate', async function () id: 1, avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4', login: 'user-1', + username: 'user-1', name: 'User 1', url: 'https://github.com/user-1', }, diff --git a/packages/crates-io-msw/models/api-token.test.js b/packages/crates-io-msw/models/api-token.test.js index 135b78352b9..867503fdc99 100644 --- a/packages/crates-io-msw/models/api-token.test.js +++ b/packages/crates-io-msw/models/api-token.test.js @@ -34,6 +34,7 @@ test('happy path', ({ expect }) => { "name": "User 1", "publishNotifications": true, "url": "https://github.com/user-1", + "username": "user-1", Symbol(type): "user", Symbol(primaryKey): "id", }, diff --git a/packages/crates-io-msw/models/crate-owner-invitation.test.js b/packages/crates-io-msw/models/crate-owner-invitation.test.js index 32c10b97d52..4e9725b7f1f 100644 --- a/packages/crates-io-msw/models/crate-owner-invitation.test.js +++ b/packages/crates-io-msw/models/crate-owner-invitation.test.js @@ -66,6 +66,7 @@ test('happy path', ({ expect }) => { "name": "User 2", "publishNotifications": true, "url": "https://github.com/user-2", + "username": "user-2", Symbol(type): "user", Symbol(primaryKey): "id", }, @@ -81,6 +82,7 @@ test('happy path', ({ expect }) => { "name": "User 1", "publishNotifications": true, "url": "https://github.com/user-1", + "username": "user-1", Symbol(type): "user", Symbol(primaryKey): "id", }, diff --git a/packages/crates-io-msw/models/crate-ownership.test.js b/packages/crates-io-msw/models/crate-ownership.test.js index 00f6b389824..3ef87c2d345 100644 --- a/packages/crates-io-msw/models/crate-ownership.test.js +++ b/packages/crates-io-msw/models/crate-ownership.test.js @@ -58,6 +58,7 @@ test('can set `team`', ({ expect }) => { "name": "team-1", "org": "rust-lang", "url": "https://github.com/rust-lang", + "username": "github:rust-lang:team-1", Symbol(type): "team", Symbol(primaryKey): "id", }, @@ -107,6 +108,7 @@ test('can set `user`', ({ expect }) => { "name": "User 1", "publishNotifications": true, "url": "https://github.com/user-1", + "username": "user-1", Symbol(type): "user", Symbol(primaryKey): "id", }, diff --git a/packages/crates-io-msw/models/msw-session.test.js b/packages/crates-io-msw/models/msw-session.test.js index 5a6874566c9..d01a15f300c 100644 --- a/packages/crates-io-msw/models/msw-session.test.js +++ b/packages/crates-io-msw/models/msw-session.test.js @@ -24,6 +24,7 @@ test('happy path', ({ expect }) => { "name": "User 1", "publishNotifications": true, "url": "https://github.com/user-1", + "username": "user-1", Symbol(type): "user", Symbol(primaryKey): "id", }, diff --git a/packages/crates-io-msw/models/team.js b/packages/crates-io-msw/models/team.js index 87df22a6f80..e7eea285e24 100644 --- a/packages/crates-io-msw/models/team.js +++ b/packages/crates-io-msw/models/team.js @@ -10,6 +10,7 @@ export default { name: String, org: String, login: String, + username: String, url: String, avatar: String, @@ -18,6 +19,7 @@ export default { applyDefault(attrs, 'name', () => `team-${attrs.id}`); applyDefault(attrs, 'org', () => ORGS[(attrs.id - 1) % ORGS.length]); applyDefault(attrs, 'login', () => `github:${attrs.org}:${attrs.name}`); + applyDefault(attrs, 'username', () => `github:${attrs.org}:${attrs.name}`); applyDefault(attrs, 'url', () => `https://github.com/${attrs.org}`); applyDefault(attrs, 'avatar', () => 'https://avatars1.githubusercontent.com/u/14631425?v=4'); }, diff --git a/packages/crates-io-msw/models/team.test.js b/packages/crates-io-msw/models/team.test.js index c3aecafd0f1..c5c39ed7bb2 100644 --- a/packages/crates-io-msw/models/team.test.js +++ b/packages/crates-io-msw/models/team.test.js @@ -12,6 +12,7 @@ test('default are applied', ({ expect }) => { "name": "team-1", "org": "rust-lang", "url": "https://github.com/rust-lang", + "username": "github:rust-lang:team-1", Symbol(type): "team", Symbol(primaryKey): "id", } @@ -28,6 +29,7 @@ test('attributes can be set', ({ expect }) => { "name": "axum", "org": "tokio-rs", "url": "https://github.com/tokio-rs", + "username": "github:tokio-rs:axum", Symbol(type): "team", Symbol(primaryKey): "id", } diff --git a/packages/crates-io-msw/models/user.js b/packages/crates-io-msw/models/user.js index 2d3ce6f22f4..9e2999fb91c 100644 --- a/packages/crates-io-msw/models/user.js +++ b/packages/crates-io-msw/models/user.js @@ -8,6 +8,7 @@ export default { name: nullable(String), login: String, + username: String, url: String, avatar: String, email: nullable(String), @@ -22,7 +23,8 @@ export default { applyDefault(attrs, 'id', () => counter); applyDefault(attrs, 'name', () => `User ${attrs.id}`); applyDefault(attrs, 'login', () => (attrs.name ? dasherize(attrs.name) : `user-${attrs.id}`)); - applyDefault(attrs, 'email', () => `${attrs.login}@crates.io`); + applyDefault(attrs, 'username', () => (attrs.name ? dasherize(attrs.name) : `user-${attrs.id}`)); + applyDefault(attrs, 'email', () => `${attrs.username}@crates.io`); applyDefault(attrs, 'url', () => `https://github.com/${attrs.login}`); applyDefault(attrs, 'avatar', () => 'https://avatars1.githubusercontent.com/u/14631425?v=4'); applyDefault(attrs, 'emailVerificationToken', () => null); diff --git a/packages/crates-io-msw/models/user.test.js b/packages/crates-io-msw/models/user.test.js index e3db559e569..e15bba659d6 100644 --- a/packages/crates-io-msw/models/user.test.js +++ b/packages/crates-io-msw/models/user.test.js @@ -17,6 +17,7 @@ test('default are applied', ({ expect }) => { "name": "User 1", "publishNotifications": true, "url": "https://github.com/user-1", + "username": "user-1", Symbol(type): "user", Symbol(primaryKey): "id", } @@ -38,6 +39,7 @@ test('name can be set', ({ expect }) => { "name": "John Doe", "publishNotifications": true, "url": "https://github.com/john-doe", + "username": "john-doe", Symbol(type): "user", Symbol(primaryKey): "id", } diff --git a/src/controllers/krate/owners.rs b/src/controllers/krate/owners.rs index e3482ba6e59..0bebd0cd3cb 100644 --- a/src/controllers/krate/owners.rs +++ b/src/controllers/krate/owners.rs @@ -328,7 +328,7 @@ async fn invite_user_owner( let user = User::find_by_login(conn, login) .await .optional()? - .ok_or_else(|| bad_request(format_args!("could not find user with login `{login}`")))?; + .ok_or_else(|| bad_request(format_args!("could not find user with username `{login}`")))?; // Users are invited and must accept before being added let expires_at = Utc::now() + app.config.ownership_invitations_expiration; @@ -498,7 +498,7 @@ impl From for BoxedAppError { match error { OwnerRemoveError::Diesel(error) => error.into(), OwnerRemoveError::NotFound { login } => { - bad_request(format!("could not find owner with login `{login}`")) + bad_request(format!("could not find owner with username `{login}`")) } } } diff --git a/src/tests/issues/issue1205.rs b/src/tests/issues/issue1205.rs index 1229746dc0e..01260cd8209 100644 --- a/src/tests/issues/issue1205.rs +++ b/src/tests/issues/issue1205.rs @@ -46,7 +46,7 @@ async fn test_issue_1205() -> anyhow::Result<()> { .remove_named_owner(CRATE_NAME, "github:rustaudio:owners") .await; assert_eq!(response.status(), StatusCode::BAD_REQUEST); - assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"could not find owner with login `github:rustaudio:owners`"}]}"#); + assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"could not find owner with username `github:rustaudio:owners`"}]}"#); Ok(()) } diff --git a/src/tests/routes/crates/owners/add.rs b/src/tests/routes/crates/owners/add.rs index f2aa0a8f02c..79649abebdc 100644 --- a/src/tests/routes/crates/owners/add.rs +++ b/src/tests/routes/crates/owners/add.rs @@ -305,7 +305,7 @@ async fn test_unknown_user() { let response = cookie.add_named_owner("foo", "unknown").await; assert_eq!(response.status(), StatusCode::BAD_REQUEST); - assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"could not find user with login `unknown`"}]}"#); + assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"could not find user with username `unknown`"}]}"#); } #[tokio::test(flavor = "multi_thread")] @@ -370,7 +370,7 @@ async fn no_invite_emails_for_txn_rollback() { let response = token.add_named_owners("crate_name", &usernames).await; assert_eq!(response.status(), StatusCode::BAD_REQUEST); - assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"could not find user with login `bananas`"}]}"#); + assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"could not find user with username `bananas`"}]}"#); // No emails should have been sent. assert_eq!(app.emails().await.len(), 0); diff --git a/src/tests/routes/crates/owners/remove.rs b/src/tests/routes/crates/owners/remove.rs index 669248aef07..9ef01b23a5a 100644 --- a/src/tests/routes/crates/owners/remove.rs +++ b/src/tests/routes/crates/owners/remove.rs @@ -61,7 +61,7 @@ async fn test_unknown_user() { let response = cookie.remove_named_owner("foo", "unknown").await; assert_eq!(response.status(), StatusCode::BAD_REQUEST); - assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"could not find owner with login `unknown`"}]}"#); + assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"could not find owner with username `unknown`"}]}"#); } #[tokio::test(flavor = "multi_thread")] @@ -77,7 +77,7 @@ async fn test_unknown_team() { .remove_named_owner("foo", "github:unknown:unknown") .await; assert_eq!(response.status(), StatusCode::BAD_REQUEST); - assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"could not find owner with login `github:unknown:unknown`"}]}"#); + assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"could not find owner with username `github:unknown:unknown`"}]}"#); } #[tokio::test(flavor = "multi_thread")] diff --git a/src/tests/routes/users/read.rs b/src/tests/routes/users/read.rs index 170aec9906b..9564e966860 100644 --- a/src/tests/routes/users/read.rs +++ b/src/tests/routes/users/read.rs @@ -16,9 +16,11 @@ async fn show() { let json: UserShowPublicResponse = anon.get("/api/v1/users/foo").await.good(); assert_eq!(json.user.login, "foo"); + assert_eq!(json.user.username, "foo"); let json: UserShowPublicResponse = anon.get("/api/v1/users/bAr").await.good(); assert_eq!(json.user.login, "Bar"); + assert_eq!(json.user.username, "Bar"); assert_eq!(json.user.url, "https://github.com/Bar"); } diff --git a/src/tests/team.rs b/src/tests/team.rs index 34561a20cc0..eb4303f9467 100644 --- a/src/tests/team.rs +++ b/src/tests/team.rs @@ -121,6 +121,7 @@ async fn add_renamed_team() -> anyhow::Result<()> { let json = anon.crate_owner_teams("foo_renamed_team").await.good(); assert_eq!(json.teams.len(), 1); assert_eq!(json.teams[0].login, "github:test-org:core"); + assert_eq!(json.teams[0].username, "github:test-org:core"); Ok(()) } @@ -151,6 +152,7 @@ async fn add_team_mixed_case() -> anyhow::Result<()> { let json = anon.crate_owner_teams("foo_mixed_case").await.good(); assert_eq!(json.teams.len(), 1); assert_eq!(json.teams[0].login, "github:test-org:core"); + assert_eq!(json.teams[0].username, "github:test-org:core"); Ok(()) } diff --git a/tests/acceptance/api-token-test.js b/tests/acceptance/api-token-test.js index 1c18fcac5ac..c6d8c092c93 100644 --- a/tests/acceptance/api-token-test.js +++ b/tests/acceptance/api-token-test.js @@ -14,6 +14,7 @@ module('Acceptance | api-tokens', function (hooks) { function prepare(context) { let user = context.db.user.create({ login: 'johnnydee', + username: 'johnnydee', name: 'John Doe', email: 'john@doe.com', avatar: 'https://avatars2.githubusercontent.com/u/1234567?v=4', diff --git a/tests/acceptance/crate-test.js b/tests/acceptance/crate-test.js index d3e03704ba9..52127b3f428 100644 --- a/tests/acceptance/crate-test.js +++ b/tests/acceptance/crate-test.js @@ -215,7 +215,7 @@ module('Acceptance | crate page', function (hooks) { skip('crates can be yanked by owner', async function (assert) { loadFixtures(this.db); - let user = this.db.user.findFirst({ where: { login: { equals: 'thehydroimpulse' } } }); + let user = this.db.user.findFirst({ where: { username: { equals: 'thehydroimpulse' } } }); this.authenticateAs(user); await visit('/crates/nanomsg/0.5.0'); @@ -242,7 +242,7 @@ module('Acceptance | crate page', function (hooks) { test('navigating to the owners page when not an owner', async function (assert) { loadFixtures(this.db); - let user = this.db.user.findFirst({ where: { login: { equals: 'iain8' } } }); + let user = this.db.user.findFirst({ where: { username: { equals: 'iain8' } } }); this.authenticateAs(user); await visit('/crates/nanomsg'); @@ -253,7 +253,7 @@ module('Acceptance | crate page', function (hooks) { test('navigating to the settings page', async function (assert) { loadFixtures(this.db); - let user = this.db.user.findFirst({ where: { login: { equals: 'thehydroimpulse' } } }); + let user = this.db.user.findFirst({ where: { username: { equals: 'thehydroimpulse' } } }); this.authenticateAs(user); await visit('/crates/nanomsg'); diff --git a/tests/acceptance/dashboard-test.js b/tests/acceptance/dashboard-test.js index 0bda0593957..e0a31b381c6 100644 --- a/tests/acceptance/dashboard-test.js +++ b/tests/acceptance/dashboard-test.js @@ -21,6 +21,7 @@ module('Acceptance | Dashboard', function (hooks) { test('shows the dashboard when logged in', async function (assert) { let user = this.db.user.create({ login: 'johnnydee', + username: 'johnnydee', name: 'John Doe', email: 'john@doe.com', avatar: 'https://avatars2.githubusercontent.com/u/1234567?v=4', diff --git a/tests/acceptance/login-test.js b/tests/acceptance/login-test.js index 7d000efe358..f3a3e2481ce 100644 --- a/tests/acceptance/login-test.js +++ b/tests/acceptance/login-test.js @@ -45,6 +45,7 @@ module('Acceptance | Login', function (hooks) { user: { id: 42, login: 'johnnydee', + username: 'johnnydee', name: 'John Doe', email: 'john@doe.name', avatar: 'https://avatars2.githubusercontent.com/u/12345?v=4', diff --git a/tests/acceptance/settings/add-owner-test.js b/tests/acceptance/settings/add-owner-test.js index 50ae4130701..1461a8ee73e 100644 --- a/tests/acceptance/settings/add-owner-test.js +++ b/tests/acceptance/settings/add-owner-test.js @@ -43,7 +43,7 @@ module('Acceptance | Settings | Add Owner', function (hooks) { assert .dom('[data-test-notification-message="error"]') - .hasText('Error sending invite: could not find user with login `spookyghostboo`'); + .hasText('Error sending invite: could not find user with username `spookyghostboo`'); assert.dom('[data-test-owners] [data-test-owner-team]').exists({ count: 2 }); assert.dom('[data-test-owners] [data-test-owner-user]').exists({ count: 2 }); }); diff --git a/tests/acceptance/settings/remove-owner-test.js b/tests/acceptance/settings/remove-owner-test.js index 5bcb55973ed..5814eeabfe7 100644 --- a/tests/acceptance/settings/remove-owner-test.js +++ b/tests/acceptance/settings/remove-owner-test.js @@ -48,11 +48,11 @@ module('Acceptance | Settings | Remove Owner', function (hooks) { ); await visit(`/crates/${crate.name}/settings`); - await click(`[data-test-owner-user="${user2.login}"] [data-test-remove-owner-button]`); + await click(`[data-test-owner-user="${user2.username}"] [data-test-remove-owner-button]`); assert .dom('[data-test-notification-message="error"]') - .hasText(`Failed to remove the user ${user2.login} as crate owner: nope`); + .hasText(`Failed to remove the user ${user2.username} as crate owner: nope`); assert.dom('[data-test-owner-user]').exists({ count: 2 }); }); @@ -76,7 +76,7 @@ module('Acceptance | Settings | Remove Owner', function (hooks) { ); await visit(`/crates/${crate.name}/settings`); - await click(`[data-test-owner-team="${team1.login}"] [data-test-remove-owner-button]`); + await click(`[data-test-owner-team="${team1.username}"] [data-test-remove-owner-button]`); assert .dom('[data-test-notification-message="error"]') diff --git a/tests/acceptance/sudo-test.js b/tests/acceptance/sudo-test.js index 274fc7ae412..9bca55687a1 100644 --- a/tests/acceptance/sudo-test.js +++ b/tests/acceptance/sudo-test.js @@ -13,6 +13,7 @@ module('Acceptance | sudo', function (hooks) { function prepare(context, isAdmin) { const user = context.db.user.create({ login: 'johnnydee', + username: 'johnnydee', name: 'John Doe', email: 'john@doe.com', avatar: 'https://avatars2.githubusercontent.com/u/1234567?v=4', diff --git a/tests/components/owners-list-test.js b/tests/components/owners-list-test.js index 5e44df4cd74..fdcbd6c64dd 100644 --- a/tests/components/owners-list-test.js +++ b/tests/components/owners-list-test.js @@ -26,8 +26,8 @@ module('Component | OwnersList', function (hooks) { assert.dom('ul > li').exists({ count: 1 }); assert.dom('[data-test-owner-link]').exists({ count: 1 }); - let logins = [...this.element.querySelectorAll('[data-test-owner-link]')].map(it => it.dataset.testOwnerLink); - assert.deepEqual(logins, ['user-1']); + let usernames = [...this.element.querySelectorAll('[data-test-owner-link]')].map(it => it.dataset.testOwnerLink); + assert.deepEqual(usernames, ['user-1']); assert.dom('[data-test-owner-link="user-1"]').hasText('User 1'); assert.dom('[data-test-owner-link="user-1"]').hasAttribute('href', '/users/user-1'); @@ -37,7 +37,7 @@ module('Component | OwnersList', function (hooks) { let crate = this.db.crate.create(); this.db.version.create({ crate }); - let user = this.db.user.create({ name: null, login: 'anonymous' }); + let user = this.db.user.create({ name: null, username: 'anonymous' }); this.db.crateOwnership.create({ crate, user }); let store = this.owner.lookup('service:store'); @@ -49,8 +49,8 @@ module('Component | OwnersList', function (hooks) { assert.dom('ul > li').exists({ count: 1 }); assert.dom('[data-test-owner-link]').exists({ count: 1 }); - let logins = [...this.element.querySelectorAll('[data-test-owner-link]')].map(it => it.dataset.testOwnerLink); - assert.deepEqual(logins, ['anonymous']); + let usernames = [...this.element.querySelectorAll('[data-test-owner-link]')].map(it => it.dataset.testOwnerLink); + assert.deepEqual(usernames, ['anonymous']); assert.dom('[data-test-owner-link="anonymous"]').hasText('anonymous'); assert.dom('[data-test-owner-link="anonymous"]').hasAttribute('href', '/users/anonymous'); @@ -74,8 +74,8 @@ module('Component | OwnersList', function (hooks) { assert.dom('ul > li').exists({ count: 5 }); assert.dom('[data-test-owner-link]').exists({ count: 5 }); - let logins = [...this.element.querySelectorAll('[data-test-owner-link]')].map(it => it.dataset.testOwnerLink); - assert.deepEqual(logins, ['user-1', 'user-2', 'user-3', 'user-4', 'user-5']); + let usernames = [...this.element.querySelectorAll('[data-test-owner-link]')].map(it => it.dataset.testOwnerLink); + assert.deepEqual(usernames, ['user-1', 'user-2', 'user-3', 'user-4', 'user-5']); }); test('six users', async function (assert) { @@ -96,8 +96,8 @@ module('Component | OwnersList', function (hooks) { assert.dom('ul > li').exists({ count: 6 }); assert.dom('[data-test-owner-link]').exists({ count: 6 }); - let logins = [...this.element.querySelectorAll('[data-test-owner-link]')].map(it => it.dataset.testOwnerLink); - assert.deepEqual(logins, ['user-1', 'user-2', 'user-3', 'user-4', 'user-5', 'user-6']); + let usernames = [...this.element.querySelectorAll('[data-test-owner-link]')].map(it => it.dataset.testOwnerLink); + assert.deepEqual(usernames, ['user-1', 'user-2', 'user-3', 'user-4', 'user-5', 'user-6']); }); test('teams mixed with users', async function (assert) { @@ -122,8 +122,8 @@ module('Component | OwnersList', function (hooks) { assert.dom('ul > li').exists({ count: 5 }); assert.dom('[data-test-owner-link]').exists({ count: 5 }); - let logins = [...this.element.querySelectorAll('[data-test-owner-link]')].map(it => it.dataset.testOwnerLink); - assert.deepEqual(logins, ['github:crates-io:team-1', 'github:crates-io:team-2', 'user-1', 'user-2', 'user-3']); + let usernames = [...this.element.querySelectorAll('[data-test-owner-link]')].map(it => it.dataset.testOwnerLink); + assert.deepEqual(usernames, ['github:crates-io:team-1', 'github:crates-io:team-2', 'user-1', 'user-2', 'user-3']); assert.dom('[data-test-owner-link="github:crates-io:team-1"]').hasText('crates-io/team-1'); assert diff --git a/tests/models/crate-test.js b/tests/models/crate-test.js index bb2e0a8dfde..d677293abc5 100644 --- a/tests/models/crate-test.js +++ b/tests/models/crate-test.js @@ -25,7 +25,7 @@ module('Model | Crate', function (hooks) { let crateRecord = await this.store.findRecord('crate', crate.name); - let result = await crateRecord.inviteOwner(user2.login); + let result = await crateRecord.inviteOwner(user2.username); assert.deepEqual(result, { ok: true, msg: 'user user-2 has been invited to be an owner of crate crate-1' }); }); @@ -39,7 +39,7 @@ module('Model | Crate', function (hooks) { let crateRecord = await this.store.findRecord('crate', crate.name); await assert.rejects(crateRecord.inviteOwner('unknown'), function (error) { - assert.deepEqual(error.errors, [{ detail: 'could not find user with login `unknown`' }]); + assert.deepEqual(error.errors, [{ detail: 'could not find user with username `unknown`' }]); return true; }); }); @@ -58,7 +58,7 @@ module('Model | Crate', function (hooks) { let crateRecord = await this.store.findRecord('crate', crate.name); - let result = await crateRecord.removeOwner(user2.login); + let result = await crateRecord.removeOwner(user2.username); assert.deepEqual(result, { ok: true, msg: 'owners successfully removed' }); }); diff --git a/tests/routes/settings/tokens/index-test.js b/tests/routes/settings/tokens/index-test.js index 042442a7d14..29032c96272 100644 --- a/tests/routes/settings/tokens/index-test.js +++ b/tests/routes/settings/tokens/index-test.js @@ -11,6 +11,7 @@ module('/settings/tokens', function (hooks) { function prepare(context) { let user = context.db.user.create({ login: 'johnnydee', + username: 'johnnydee', name: 'John Doe', email: 'john@doe.com', avatar: 'https://avatars2.githubusercontent.com/u/1234567?v=4', diff --git a/tests/routes/settings/tokens/new-test.js b/tests/routes/settings/tokens/new-test.js index f1126c0ac34..5183517b044 100644 --- a/tests/routes/settings/tokens/new-test.js +++ b/tests/routes/settings/tokens/new-test.js @@ -15,6 +15,7 @@ module('/settings/tokens/new', function (hooks) { function prepare(context) { let user = context.db.user.create({ login: 'johnnydee', + username: 'johnnydee', name: 'John Doe', email: 'john@doe.com', avatar: 'https://avatars2.githubusercontent.com/u/1234567?v=4', From 83fac29735284943ee270a5419a7bbfd9f0697f0 Mon Sep 17 00:00:00 2001 From: "Carol (Nichols || Goulding)" Date: Tue, 4 Mar 2025 21:40:04 -0500 Subject: [PATCH 6/6] Send linked accounts in user JSON (deploy 6) --- crates/crates_io_database/src/models/user.rs | 29 +++++++ src/controllers/user/other.rs | 10 ++- ..._io__openapi__tests__openapi_snapshot.snap | 15 +++- src/tests/routes/users/read.rs | 7 ++ src/tests/util/test_app.rs | 23 +++-- src/views.rs | 87 ++++++++++++++++++- 6 files changed, 156 insertions(+), 15 deletions(-) diff --git a/crates/crates_io_database/src/models/user.rs b/crates/crates_io_database/src/models/user.rs index b7b8511caca..8b85a1dad32 100644 --- a/crates/crates_io_database/src/models/user.rs +++ b/crates/crates_io_database/src/models/user.rs @@ -11,6 +11,8 @@ use crate::models::{Crate, CrateOwner, Email, Owner, OwnerKind}; use crate::schema::{crate_owners, emails, linked_accounts, users}; use crates_io_diesel_helpers::{lower, pg_enum}; +use std::fmt::{Display, Formatter}; + /// The model representing a row in the `users` database table. #[derive(Clone, Debug, Queryable, Identifiable, Selectable)] pub struct User { @@ -77,6 +79,17 @@ impl User { .await .optional() } + + /// Queries for the linked accounts belonging to a particular user + pub async fn linked_accounts( + &self, + conn: &mut AsyncPgConnection, + ) -> QueryResult> { + LinkedAccount::belonging_to(self) + .select(LinkedAccount::as_select()) + .load(conn) + .await + } } /// Represents a new user record insertable to the `users` table @@ -133,6 +146,22 @@ pg_enum! { } } +impl Display for AccountProvider { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::Github => write!(f, "GitHub"), + } + } +} + +impl AccountProvider { + pub fn url(&self, login: &str) -> String { + match self { + Self::Github => format!("https://github.com/{login}"), + } + } +} + /// Represents an OAuth account record linked to a user record. #[derive(Associations, Identifiable, Selectable, Queryable, Debug, Clone)] #[diesel( diff --git a/src/controllers/user/other.rs b/src/controllers/user/other.rs index b0ac93b29ba..f903a32b395 100644 --- a/src/controllers/user/other.rs +++ b/src/controllers/user/other.rs @@ -16,12 +16,12 @@ pub struct GetResponse { pub user: EncodablePublicUser, } -/// Find user by login. +/// Find user by username. #[utoipa::path( get, path = "/api/v1/users/{user}", params( - ("user" = String, Path, description = "Login name of the user"), + ("user" = String, Path, description = "Crates.io username of the user"), ), tag = "users", responses((status = 200, description = "Successful Response", body = inline(GetResponse))), @@ -41,7 +41,11 @@ pub async fn find_user( .first(&mut conn) .await?; - Ok(Json(GetResponse { user: user.into() })) + let linked_accounts = user.linked_accounts(&mut conn).await?; + + Ok(Json(GetResponse { + user: EncodablePublicUser::with_linked_accounts(user, &linked_accounts), + })) } #[derive(Debug, Serialize, utoipa::ToSchema)] diff --git a/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap b/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap index 049a91a098b..e2f3ea8bce8 100644 --- a/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap +++ b/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap @@ -865,6 +865,17 @@ snapshot_kind: text "format": "int32", "type": "integer" }, + "linked_accounts": { + "description": "The accounts linked to this crates.io account.", + "example": [], + "items": { + "$ref": "#/components/schemas/LinkedAccount" + }, + "type": [ + "array", + "null" + ] + }, "login": { "description": "The user's GitHub login name.", "example": "ghost", @@ -4167,7 +4178,7 @@ snapshot_kind: text "operationId": "find_user", "parameters": [ { - "description": "Login name of the user", + "description": "Crates.io username of the user", "in": "path", "name": "user", "required": true, @@ -4196,7 +4207,7 @@ snapshot_kind: text "description": "Successful Response" } }, - "summary": "Find user by login.", + "summary": "Find user by username.", "tags": [ "users" ] diff --git a/src/tests/routes/users/read.rs b/src/tests/routes/users/read.rs index 9564e966860..84aba7a33a7 100644 --- a/src/tests/routes/users/read.rs +++ b/src/tests/routes/users/read.rs @@ -22,6 +22,13 @@ async fn show() { assert_eq!(json.user.login, "Bar"); assert_eq!(json.user.username, "Bar"); assert_eq!(json.user.url, "https://github.com/Bar"); + + let accounts = json.user.linked_accounts.unwrap(); + assert_eq!(accounts.len(), 1); + let account = &accounts[0]; + assert_eq!(account.provider, "GitHub"); + assert_eq!(account.login, "Bar"); + assert_eq!(account.url, "https://github.com/Bar"); } #[tokio::test(flavor = "multi_thread")] diff --git a/src/tests/util/test_app.rs b/src/tests/util/test_app.rs index 2e155b3332b..b0b43865b4d 100644 --- a/src/tests/util/test_app.rs +++ b/src/tests/util/test_app.rs @@ -3,8 +3,8 @@ use crate::config::{ self, Base, CdnLogQueueConfig, CdnLogStorageConfig, DatabasePools, DbPoolConfig, }; use crate::middleware::cargo_compat::StatusCodeConfig; -use crate::models::NewEmail; use crate::models::token::{CrateScope, EndpointScope}; +use crate::models::{AccountProvider, NewEmail, NewLinkedAccount}; use crate::rate_limiter::{LimitedAction, RateLimiterConfig}; use crate::storage::StorageConfig; use crate::tests::util::chaosproxy::ChaosProxy; @@ -114,8 +114,8 @@ impl TestApp { self.0.test_database.async_connect().await } - /// Create a new user with a verified email address in the database - /// (`@example.com`) and return a mock user session. + /// Create a new user with a verified email address (`@example.com`) + /// and a linked GitHub account in the database and return a mock user session. /// /// This method updates the database directly pub async fn db_new_user(&self, username: &str) -> MockCookieUser { @@ -123,10 +123,19 @@ impl TestApp { let email = format!("{username}@example.com"); - let user = crate::tests::new_user(username) - .insert(&mut conn) - .await - .unwrap(); + let new_user = crate::tests::new_user(username); + let user = new_user.insert(&mut conn).await.unwrap(); + + let linked_account = NewLinkedAccount::builder() + .user_id(user.id) + .provider(AccountProvider::Github) + .account_id(user.gh_id) + .access_token(&new_user.gh_access_token) + .login(&user.gh_login) + .maybe_avatar(user.gh_avatar.as_deref()) + .build(); + + linked_account.insert_or_update(&mut conn).await.unwrap(); let new_email = NewEmail::builder() .user_id(user.id) diff --git a/src/views.rs b/src/views.rs index 1f3089cc16c..824c563c36e 100644 --- a/src/views.rs +++ b/src/views.rs @@ -2,8 +2,8 @@ use chrono::{DateTime, Utc}; use crate::external_urls::remove_blocked_urls; use crate::models::{ - ApiToken, Category, Crate, Dependency, DependencyKind, Keyword, Owner, ReverseDependency, Team, - TopVersions, User, Version, VersionDownload, VersionOwnerAction, + ApiToken, Category, Crate, Dependency, DependencyKind, Keyword, LinkedAccount, Owner, + ReverseDependency, Team, TopVersions, User, Version, VersionDownload, VersionOwnerAction, }; use crates_io_github as github; @@ -790,9 +790,15 @@ pub struct EncodablePublicUser { /// The user's GitHub profile URL. #[schema(example = "https://github.com/ghost")] pub url: String, + + /// The accounts linked to this crates.io account. + #[serde(skip_serializing_if = "Option::is_none")] + #[schema(no_recursion, example = json!([]))] + pub linked_accounts: Option>, } -/// Converts a `User` model into an `EncodablePublicUser` for JSON serialization. +/// Converts a `User` model into an `EncodablePublicUser` for JSON serialization. Does not include +/// linked accounts. impl From for EncodablePublicUser { fn from(user: User) -> Self { let User { @@ -805,6 +811,38 @@ impl From for EncodablePublicUser { } = user; let url = format!("https://github.com/{gh_login}"); let username = username.unwrap_or(gh_login); + + EncodablePublicUser { + id, + login: username.clone(), + username, + name, + avatar: gh_avatar, + url, + linked_accounts: None, + } + } +} + +impl EncodablePublicUser { + pub fn with_linked_accounts(user: User, linked_accounts: &[LinkedAccount]) -> Self { + let User { + id, + name, + username, + gh_login, + gh_avatar, + .. + } = user; + let url = format!("https://github.com/{gh_login}"); + let username = username.unwrap_or(gh_login); + + let linked_accounts = if linked_accounts.is_empty() { + None + } else { + Some(linked_accounts.iter().map(Into::into).collect()) + }; + EncodablePublicUser { id, login: username.clone(), @@ -812,6 +850,48 @@ impl From for EncodablePublicUser { name, avatar: gh_avatar, url, + linked_accounts, + } + } +} + +#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, utoipa::ToSchema)] +#[schema(as = LinkedAccount)] +pub struct EncodableLinkedAccount { + /// The service providing this linked account. + #[schema(example = "GitHub")] + pub provider: String, + + /// The linked account's login name. + #[schema(example = "ghost")] + pub login: String, + + /// The linked account's avatar URL, if set. + #[schema(example = "https://avatars2.githubusercontent.com/u/1234567?v=4")] + pub avatar: Option, + + /// The linked account's profile URL on the provided service. + #[schema(example = "https://github.com/ghost")] + pub url: String, +} + +/// Converts a `LinkedAccount` model into an `EncodableLinkedAccount` for JSON serialization. +impl From<&LinkedAccount> for EncodableLinkedAccount { + fn from(linked_account: &LinkedAccount) -> Self { + let LinkedAccount { + provider, + login, + avatar, + .. + } = linked_account; + + let url = provider.url(login); + + Self { + provider: provider.to_string(), + login: login.clone(), + avatar: avatar.clone(), + url, } } } @@ -1140,6 +1220,7 @@ mod tests { name: None, avatar: None, url: String::new(), + linked_accounts: None, }, time: NaiveDate::from_ymd_opt(2017, 1, 6) .unwrap()