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 2b6f2698ad..7b75204056 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 9d060361ca..48ec3d35b2 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 9b05b2434e..bccecbb2fe 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 e42d45c59d..95199a9046 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 1d801f192d..79b024b9d3 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 f5315ad692..6da832e4c4 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 0000000000..bef2a0aa5e --- /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 0000000000..aa2d79a9fc --- /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 44266c356a..a1deda256d 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 c7d0e549ac..bce2be568e 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 33e9879684..6e9cb69022 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 0000000000..f2413cd110 --- /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 48ec3d35b2..b7b8511cac 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 bccecbb2fe..ccf182b95e 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 95199a9046..9213d7ad5c 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 79b024b9d3..41039804ec 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 6da832e4c4..20a067d590 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 0000000000..228952f6bf --- /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 0000000000..661c0587e8 --- /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 24547c0e65..6fd88928e1 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 a1deda256d..8dd53424e0 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 0e52eb9196..08eacf2266 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 5416a9ffaa..4b4da12db2 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 cb7a074036..049a91a098 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 02666658d2..ce18851935 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 e42da14510..3c046059c4 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 6f61d116c5..6b939f6d35 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 328af1f7ec..070885d4fc 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 9e8b8ef7e7..6bd08f4220 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 896ce3ae55..3cc8890668 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 896ce3ae55..3cc8890668 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 e6938557e7..2a931ebc0d 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 e6938557e7..2a931ebc0d 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 9e8b8ef7e7..6bd08f4220 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 9d0ee1983a..5b642f6327 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 c0c7f892a3..51870813cc 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 c7b079c37a..a06971a899 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 75c2cd698b..a4a002164e 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 30269ad692..b20d9bf3bf 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 27e4773acf..0b71ee6675 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 7ca7deae51..7e06a81105 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 94dde205c9..dd548de43c 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 6850cb4856..8c36368987 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 6e17a2fbc8..ab9cafea9d 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 6e17a2fbc8..ab9cafea9d 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 d0546ef9c9..6a78599d2d 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 6940293e5c..96f047cf70 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 5564b16de5..0275f95e5f 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 b0ffc3e7fc..5d511698d0 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 f81f56e525..8f15a33467 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 cd697fc53e..170aec9906 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 6e9cb69022..e3b662ac03 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 69875e1261..ede066dfdf 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 c3b3be8443..1f3089cc16 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 a2d676c594..5cc20a90c6 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 4119eab29a..1c4add805a 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 0000000000..59372812ce --- /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 a4eac17b56..c70170064f 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 bad2d9e790..01992cf1cf 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 37012f4738..c65ba3daf0 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 13f128379c..58865fd2dd 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 e663003642..c1eaead5a1 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 72d38bdce9..e55bb789cd 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 2d69e1a108..07104f4066 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 9e96f47adc..22dbff658e 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 baf5a0af7b..37f6628903 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 5487e88876..4d6be01652 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 c4ab8eb42e..16f3ca2e1b 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 10b8c20297..c70ae7d95a 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 c7a3f2827d..c89819e76c 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 bf09fe3628..d7a7740474 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 1baefd5692..1ce2bffe78 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 aaeb970474..ab9a9bc2b4 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 43f53cbdf0..0368096bb1 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 f767ed711c..093ad53b7f 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 3970bbd60d..23a0439ff5 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 6366161629..1bb42528f5 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 87b1a8bfb8..be46a7513c 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 0c75378844..dbd2ca2546 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 00243bdce1..d6628f4b19 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 227d27ef6a..77f9bcdda4 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 cdaae3e7ae..e936b6402d 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 a221967d4f..2a9d8f9c7b 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 d1c8499822..ac5811aef7 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 25a9981aff..ea8cb1570f 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 e542615367..1cbfcc28ab 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 91e7b4c768..80ca10e7d0 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 551bcd0590..9f5df26ecb 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 8fb593aa11..8e1a70a71d 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 7e164e26f7..35ae9f68d7 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 ee299d708d..ef4a68caf7 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 f531418674..e5e7476e26 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 fba3992601..f480abf57d 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 263d0f0d00..372e015cbe 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 135b78352b..867503fdc9 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 32c10b97d5..4e9725b7f1 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 00f6b38982..3ef87c2d34 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 5a6874566c..d01a15f300 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 87df22a6f8..e7eea285e2 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 c3aecafd0f..c5c39ed7bb 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 2d3ce6f22f..9e2999fb91 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 e3db559e56..e15bba659d 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 e3482ba6e5..0bebd0cd3c 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 1229746dc0..01260cd820 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 f2aa0a8f02..79649abebd 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 669248aef0..9ef01b23a5 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 170aec9906..9564e96686 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 34561a20cc..eb4303f946 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 1c18fcac5a..c6d8c092c9 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 d3e03704ba..52127b3f42 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 0bda059395..e0a31b381c 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 7d000efe35..f3a3e2481c 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 50ae413070..1461a8ee73 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 5bcb55973e..5814eeabfe 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 274fc7ae41..9bca55687a 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 5e44df4cd7..fdcbd6c64d 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 bb2e0a8dfd..d677293abc 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 042442a7d1..29032c9627 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 f1126c0ac3..5183517b04 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 b7b8511cac..8b85a1dad3 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 b0ac93b29b..f903a32b39 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 049a91a098..e2f3ea8bce 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 9564e96686..84aba7a33a 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 2e155b3332..b0b43865b4 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 1f3089cc16..824c563c36 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()