Skip to content

Commit 1e9f6a3

Browse files
committed
Send linked accounts in user JSON (deploy 6)
1 parent 19125bb commit 1e9f6a3

File tree

6 files changed

+156
-15
lines changed

6 files changed

+156
-15
lines changed

Diff for: crates/crates_io_database/src/models/user.rs

+29
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ use crate::models::{Crate, CrateOwner, Email, Owner, OwnerKind};
1111
use crate::schema::{crate_owners, emails, linked_accounts, users};
1212
use crates_io_diesel_helpers::{lower, pg_enum};
1313

14+
use std::fmt::{Display, Formatter};
15+
1416
/// The model representing a row in the `users` database table.
1517
#[derive(Clone, Debug, Queryable, Identifiable, Selectable)]
1618
pub struct User {
@@ -77,6 +79,17 @@ impl User {
7779
.await
7880
.optional()
7981
}
82+
83+
/// Queries for the linked accounts belonging to a particular user
84+
pub async fn linked_accounts(
85+
&self,
86+
conn: &mut AsyncPgConnection,
87+
) -> QueryResult<Vec<LinkedAccount>> {
88+
LinkedAccount::belonging_to(self)
89+
.select(LinkedAccount::as_select())
90+
.load(conn)
91+
.await
92+
}
8093
}
8194

8295
/// Represents a new user record insertable to the `users` table
@@ -133,6 +146,22 @@ pg_enum! {
133146
}
134147
}
135148

149+
impl Display for AccountProvider {
150+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
151+
match self {
152+
Self::Github => write!(f, "GitHub"),
153+
}
154+
}
155+
}
156+
157+
impl AccountProvider {
158+
pub fn url(&self, login: &str) -> String {
159+
match self {
160+
Self::Github => format!("https://github.com/{login}"),
161+
}
162+
}
163+
}
164+
136165
/// Represents an OAuth account record linked to a user record.
137166
#[derive(Associations, Identifiable, Selectable, Queryable, Debug, Clone)]
138167
#[diesel(

Diff for: src/controllers/user/other.rs

+7-3
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ pub struct GetResponse {
1616
pub user: EncodablePublicUser,
1717
}
1818

19-
/// Find user by login.
19+
/// Find user by username.
2020
#[utoipa::path(
2121
get,
2222
path = "/api/v1/users/{user}",
2323
params(
24-
("user" = String, Path, description = "Login name of the user"),
24+
("user" = String, Path, description = "Crates.io username of the user"),
2525
),
2626
tag = "users",
2727
responses((status = 200, description = "Successful Response", body = inline(GetResponse))),
@@ -41,7 +41,11 @@ pub async fn find_user(
4141
.first(&mut conn)
4242
.await?;
4343

44-
Ok(Json(GetResponse { user: user.into() }))
44+
let linked_accounts = user.linked_accounts(&mut conn).await?;
45+
46+
Ok(Json(GetResponse {
47+
user: EncodablePublicUser::with_linked_accounts(user, &linked_accounts),
48+
}))
4549
}
4650

4751
#[derive(Debug, Serialize, utoipa::ToSchema)]

Diff for: src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap

+13-2
Original file line numberDiff line numberDiff line change
@@ -865,6 +865,17 @@ snapshot_kind: text
865865
"format": "int32",
866866
"type": "integer"
867867
},
868+
"linked_accounts": {
869+
"description": "The accounts linked to this crates.io account.",
870+
"example": [],
871+
"items": {
872+
"$ref": "#/components/schemas/LinkedAccount"
873+
},
874+
"type": [
875+
"array",
876+
"null"
877+
]
878+
},
868879
"login": {
869880
"description": "The user's GitHub login name.",
870881
"example": "ghost",
@@ -4167,7 +4178,7 @@ snapshot_kind: text
41674178
"operationId": "find_user",
41684179
"parameters": [
41694180
{
4170-
"description": "Login name of the user",
4181+
"description": "Crates.io username of the user",
41714182
"in": "path",
41724183
"name": "user",
41734184
"required": true,
@@ -4196,7 +4207,7 @@ snapshot_kind: text
41964207
"description": "Successful Response"
41974208
}
41984209
},
4199-
"summary": "Find user by login.",
4210+
"summary": "Find user by username.",
42004211
"tags": [
42014212
"users"
42024213
]

Diff for: src/tests/routes/users/read.rs

+7
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ async fn show() {
2222
assert_eq!(json.user.login, "Bar");
2323
assert_eq!(json.user.username, "Bar");
2424
assert_eq!(json.user.url, "https://github.com/Bar");
25+
26+
let accounts = json.user.linked_accounts.unwrap();
27+
assert_eq!(accounts.len(), 1);
28+
let account = &accounts[0];
29+
assert_eq!(account.provider, "GitHub");
30+
assert_eq!(account.login, "Bar");
31+
assert_eq!(account.url, "https://github.com/Bar");
2532
}
2633

2734
#[tokio::test(flavor = "multi_thread")]

Diff for: src/tests/util/test_app.rs

+16-7
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use crate::config::{
33
self, Base, CdnLogQueueConfig, CdnLogStorageConfig, DatabasePools, DbPoolConfig,
44
};
55
use crate::middleware::cargo_compat::StatusCodeConfig;
6-
use crate::models::NewEmail;
6+
use crate::models::{NewEmail, NewLinkedAccount, AccountProvider};
77
use crate::models::token::{CrateScope, EndpointScope};
88
use crate::rate_limiter::{LimitedAction, RateLimiterConfig};
99
use crate::storage::StorageConfig;
@@ -114,19 +114,28 @@ impl TestApp {
114114
self.0.test_database.async_connect().await
115115
}
116116

117-
/// Create a new user with a verified email address in the database
118-
/// (`<username>@example.com`) and return a mock user session.
117+
/// Create a new user with a verified email address (`<username>@example.com`)
118+
/// and a linked GitHub account in the database and return a mock user session.
119119
///
120120
/// This method updates the database directly
121121
pub async fn db_new_user(&self, username: &str) -> MockCookieUser {
122122
let mut conn = self.db_conn().await;
123123

124124
let email = format!("{username}@example.com");
125125

126-
let user = crate::tests::new_user(username)
127-
.insert(&mut conn)
128-
.await
129-
.unwrap();
126+
let new_user = crate::tests::new_user(username);
127+
let user = new_user.insert(&mut conn).await.unwrap();
128+
129+
let linked_account = NewLinkedAccount::builder()
130+
.user_id(user.id)
131+
.provider(AccountProvider::Github)
132+
.account_id(user.gh_id)
133+
.access_token(&new_user.gh_access_token)
134+
.login(&user.gh_login)
135+
.maybe_avatar(user.gh_avatar.as_deref())
136+
.build();
137+
138+
linked_account.insert_or_update(&mut conn).await.unwrap();
130139

131140
let new_email = NewEmail::builder()
132141
.user_id(user.id)

Diff for: src/views.rs

+84-3
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ use chrono::{DateTime, Utc};
22

33
use crate::external_urls::remove_blocked_urls;
44
use crate::models::{
5-
ApiToken, Category, Crate, Dependency, DependencyKind, Keyword, Owner, ReverseDependency, Team,
6-
TopVersions, User, Version, VersionDownload, VersionOwnerAction,
5+
ApiToken, Category, Crate, Dependency, DependencyKind, Keyword, LinkedAccount, Owner,
6+
ReverseDependency, Team, TopVersions, User, Version, VersionDownload, VersionOwnerAction,
77
};
88
use crates_io_github as github;
99

@@ -790,9 +790,15 @@ pub struct EncodablePublicUser {
790790
/// The user's GitHub profile URL.
791791
#[schema(example = "https://github.com/ghost")]
792792
pub url: String,
793+
794+
/// The accounts linked to this crates.io account.
795+
#[serde(skip_serializing_if = "Option::is_none")]
796+
#[schema(no_recursion, example = json!([]))]
797+
pub linked_accounts: Option<Vec<EncodableLinkedAccount>>,
793798
}
794799

795-
/// Converts a `User` model into an `EncodablePublicUser` for JSON serialization.
800+
/// Converts a `User` model into an `EncodablePublicUser` for JSON serialization. Does not include
801+
/// linked accounts.
796802
impl From<User> for EncodablePublicUser {
797803
fn from(user: User) -> Self {
798804
let User {
@@ -805,13 +811,87 @@ impl From<User> for EncodablePublicUser {
805811
} = user;
806812
let url = format!("https://github.com/{gh_login}");
807813
let username = username.unwrap_or(gh_login);
814+
815+
EncodablePublicUser {
816+
id,
817+
login: username.clone(),
818+
username,
819+
name,
820+
avatar: gh_avatar,
821+
url,
822+
linked_accounts: None,
823+
}
824+
}
825+
}
826+
827+
impl EncodablePublicUser {
828+
pub fn with_linked_accounts(user: User, linked_accounts: &[LinkedAccount]) -> Self {
829+
let User {
830+
id,
831+
name,
832+
username,
833+
gh_login,
834+
gh_avatar,
835+
..
836+
} = user;
837+
let url = format!("https://github.com/{gh_login}");
838+
let username = username.unwrap_or(gh_login);
839+
840+
let linked_accounts = if linked_accounts.is_empty() {
841+
None
842+
} else {
843+
Some(linked_accounts.iter().map(Into::into).collect())
844+
};
845+
808846
EncodablePublicUser {
809847
id,
810848
login: username.clone(),
811849
username,
812850
name,
813851
avatar: gh_avatar,
814852
url,
853+
linked_accounts,
854+
}
855+
}
856+
}
857+
858+
#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, utoipa::ToSchema)]
859+
#[schema(as = LinkedAccount)]
860+
pub struct EncodableLinkedAccount {
861+
/// The service providing this linked account.
862+
#[schema(example = "GitHub")]
863+
pub provider: String,
864+
865+
/// The linked account's login name.
866+
#[schema(example = "ghost")]
867+
pub login: String,
868+
869+
/// The linked account's avatar URL, if set.
870+
#[schema(example = "https://avatars2.githubusercontent.com/u/1234567?v=4")]
871+
pub avatar: Option<String>,
872+
873+
/// The linked account's profile URL on the provided service.
874+
#[schema(example = "https://github.com/ghost")]
875+
pub url: String,
876+
}
877+
878+
/// Converts a `LinkedAccount` model into an `EncodableLinkedAccount` for JSON serialization.
879+
impl From<&LinkedAccount> for EncodableLinkedAccount {
880+
fn from(linked_account: &LinkedAccount) -> Self {
881+
let LinkedAccount {
882+
provider,
883+
login,
884+
avatar,
885+
..
886+
} = linked_account;
887+
888+
let url = provider.url(login);
889+
890+
Self {
891+
provider: provider.to_string(),
892+
login: login.clone(),
893+
avatar: avatar.clone(),
894+
url,
815895
}
816896
}
817897
}
@@ -1140,6 +1220,7 @@ mod tests {
11401220
name: None,
11411221
avatar: None,
11421222
url: String::new(),
1223+
linked_accounts: None,
11431224
},
11441225
time: NaiveDate::from_ymd_opt(2017, 1, 6)
11451226
.unwrap()

0 commit comments

Comments
 (0)