-
Notifications
You must be signed in to change notification settings - Fork 651
Add an API route for admins to assess crates #11528
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 10 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
02879a3
Add an API route for admins to assess crates
carols10cents d851e9d
Admin list only needs read access
carols10cents 59a762e
Add description to crate admin list
carols10cents 1746fbf
Add download count to admin list
carols10cents 30a977c
Add default version num to admin list
carols10cents 4144a3d
Get num versions from default versions table instead
carols10cents e8d7a8e
Select the default version directly rather than all versions
carols10cents b22def0
Include whether the default version is yanked
carols10cents be65713
Include whether the user email is verified rather than not including …
carols10cents f08ba7c
Extract a type to describe the select expression that's gotten compli…
carols10cents e36f29b
Use insta snapshot rather than custom type+field assertion
carols10cents File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
pub mod helpers; | ||
pub mod util; | ||
|
||
pub mod admin; | ||
pub mod category; | ||
pub mod crate_owner_invitation; | ||
pub mod git; | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
use crate::{ | ||
app::AppState, | ||
auth::AuthCheck, | ||
models::{CrateOwner, OwnerKind, User}, | ||
schema::*, | ||
util::errors::{AppResult, custom}, | ||
}; | ||
use axum::{Json, extract::Path}; | ||
use chrono::{DateTime, Utc}; | ||
use diesel::{dsl::count_star, prelude::*}; | ||
use diesel_async::RunQueryDsl; | ||
use http::{StatusCode, request::Parts}; | ||
use serde::Serialize; | ||
|
||
#[derive(Debug, Queryable, Selectable)] | ||
#[diesel(check_for_backend(diesel::pg::Pg))] | ||
struct DatabaseCrateInfo { | ||
#[diesel(select_expression = crates::columns::name)] | ||
name: String, | ||
|
||
#[diesel(select_expression = crates::columns::description)] | ||
description: Option<String>, | ||
|
||
#[diesel(select_expression = crates::columns::updated_at)] | ||
updated_at: DateTime<Utc>, | ||
|
||
#[diesel(select_expression = crate_downloads::columns::downloads.nullable())] | ||
downloads: Option<i64>, | ||
|
||
#[diesel(select_expression = recent_crate_downloads::columns::downloads.nullable())] | ||
recent_crate_downloads: Option<i64>, | ||
|
||
#[diesel(select_expression = default_versions::columns::num_versions)] | ||
num_versions: Option<i32>, | ||
|
||
#[diesel(select_expression = versions::columns::yanked)] | ||
yanked: bool, | ||
|
||
#[diesel(select_expression = versions::columns::num)] | ||
default_version_num: String, | ||
|
||
#[diesel(select_expression = versions::columns::crate_size)] | ||
crate_size: i32, | ||
|
||
#[diesel(select_expression = versions::columns::bin_names)] | ||
bin_names: Option<Vec<Option<String>>>, | ||
|
||
#[diesel(select_expression = rev_deps_subquery())] | ||
num_rev_deps: i64, | ||
} | ||
|
||
/// Handles the `GET /api/private/admin_list/{username}` endpoint. | ||
pub async fn list( | ||
state: AppState, | ||
Path(username): Path<String>, | ||
req: Parts, | ||
) -> AppResult<Json<AdminListResponse>> { | ||
let mut conn = state.db_read().await?; | ||
|
||
let auth = AuthCheck::default().check(&req, &mut conn).await?; | ||
let logged_in_user = auth.user(); | ||
|
||
if !logged_in_user.is_admin { | ||
return Err(custom( | ||
StatusCode::FORBIDDEN, | ||
"must be an admin to use this route", | ||
)); | ||
} | ||
|
||
let (user, verified, user_email) = users::table | ||
.left_join(emails::table) | ||
.filter(users::gh_login.eq(username)) | ||
.select(( | ||
User::as_select(), | ||
emails::verified.nullable(), | ||
emails::email.nullable(), | ||
)) | ||
.first::<(User, Option<bool>, Option<String>)>(&mut conn) | ||
.await?; | ||
|
||
let crates: Vec<DatabaseCrateInfo> = CrateOwner::by_owner_kind(OwnerKind::User) | ||
.inner_join(crates::table) | ||
.left_join(crate_downloads::table.on(crates::id.eq(crate_downloads::crate_id))) | ||
.left_join( | ||
recent_crate_downloads::table.on(crates::id.eq(recent_crate_downloads::crate_id)), | ||
) | ||
.inner_join(default_versions::table.on(crates::id.eq(default_versions::crate_id))) | ||
.inner_join(versions::table.on(default_versions::version_id.eq(versions::id))) | ||
.filter(crate_owners::owner_id.eq(user.id)) | ||
.select(DatabaseCrateInfo::as_select()) | ||
.order(crates::name.asc()) | ||
.load(&mut conn) | ||
.await?; | ||
|
||
let crates = crates | ||
.into_iter() | ||
.map(|database_crate_info| { | ||
let DatabaseCrateInfo { | ||
name, | ||
description, | ||
updated_at, | ||
downloads, | ||
recent_crate_downloads, | ||
num_versions, | ||
yanked, | ||
default_version_num, | ||
crate_size, | ||
bin_names, | ||
num_rev_deps, | ||
} = database_crate_info; | ||
|
||
AdminCrateInfo { | ||
name, | ||
description, | ||
updated_at, | ||
downloads: downloads.unwrap_or_default() | ||
+ recent_crate_downloads.unwrap_or_default(), | ||
num_rev_deps, | ||
num_versions: num_versions.unwrap_or_default() as usize, | ||
yanked, | ||
default_version_num, | ||
crate_size, | ||
bin_names, | ||
} | ||
}) | ||
.collect(); | ||
Ok(Json(AdminListResponse { | ||
user_email, | ||
user_email_verified: verified.unwrap_or_default(), | ||
crates, | ||
})) | ||
} | ||
|
||
#[derive(Debug, Serialize)] | ||
pub struct AdminListResponse { | ||
user_email: Option<String>, | ||
user_email_verified: bool, | ||
crates: Vec<AdminCrateInfo>, | ||
} | ||
|
||
#[derive(Debug, Serialize)] | ||
pub struct AdminCrateInfo { | ||
pub name: String, | ||
pub description: Option<String>, | ||
pub updated_at: DateTime<Utc>, | ||
pub downloads: i64, | ||
pub num_rev_deps: i64, | ||
pub num_versions: usize, | ||
pub yanked: bool, | ||
pub default_version_num: String, | ||
pub crate_size: i32, | ||
pub bin_names: Option<Vec<Option<String>>>, | ||
} | ||
|
||
/// A subquery that returns the number of reverse dependencies of a crate. | ||
/// | ||
/// **Warning:** this is an incorrect reverse dependencies query, since it | ||
/// includes the `dependencies` rows for all versions, not just the | ||
/// "default version" per crate. However, it's good enough for our | ||
/// purposes here. | ||
#[diesel::dsl::auto_type] | ||
fn rev_deps_subquery() -> _ { | ||
dependencies::table | ||
.select(count_star()) | ||
.filter(dependencies::crate_id.eq(crates::id)) | ||
.single_value() | ||
.assume_not_null() | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
use crate::{ | ||
schema::users, | ||
tests::{ | ||
builders::{CrateBuilder, VersionBuilder}, | ||
util::{RequestHelper, TestApp}, | ||
}, | ||
}; | ||
use diesel::prelude::*; | ||
use diesel_async::RunQueryDsl; | ||
use insta::assert_snapshot; | ||
|
||
#[tokio::test(flavor = "multi_thread")] | ||
async fn admin_list_by_a_non_admin_fails() { | ||
let (_app, anon, user) = TestApp::init().with_user().await; | ||
|
||
let response = anon.admin_list("anything").await; | ||
assert_snapshot!(response.status(), @"403 Forbidden"); | ||
assert_snapshot!( | ||
response.text(), | ||
@r#"{"errors":[{"detail":"this action requires authentication"}]}"# | ||
); | ||
|
||
let response = user.admin_list("anything").await; | ||
assert_snapshot!(response.status(), @"403 Forbidden"); | ||
assert_snapshot!( | ||
response.text(), | ||
@r#"{"errors":[{"detail":"must be an admin to use this route"}]}"# | ||
); | ||
} | ||
|
||
#[tokio::test(flavor = "multi_thread")] | ||
async fn index_include_yanked() -> anyhow::Result<()> { | ||
let (app, _anon, user) = TestApp::init().with_user().await; | ||
let mut conn = app.db_conn().await; | ||
let user = user.as_model(); | ||
|
||
let admin = app.db_new_user("admin").await; | ||
|
||
diesel::update(admin.as_model()) | ||
.set(users::is_admin.eq(true)) | ||
.execute(&mut conn) | ||
.await | ||
.unwrap(); | ||
|
||
let crate_1 = CrateBuilder::new("unyanked", user.id) | ||
.description("My Fun Crate") | ||
.downloads(500) | ||
.recent_downloads(36) | ||
.version(VersionBuilder::new("0.1.0").yanked(true)) | ||
.version(VersionBuilder::new("1.0.0")) | ||
.version(VersionBuilder::new("2.0.0").yanked(true)) | ||
.expect_build(&mut conn) | ||
.await; | ||
|
||
CrateBuilder::new("all_yanked", user.id) | ||
.version(VersionBuilder::new("1.0.0").yanked(true)) | ||
.version(VersionBuilder::new("2.0.0").yanked(true)) | ||
.expect_build(&mut conn) | ||
.await; | ||
|
||
CrateBuilder::new("someone_elses_crate", admin.as_model().id) | ||
.version(VersionBuilder::new("1.0.0").dependency(&crate_1, None)) | ||
.expect_build(&mut conn) | ||
.await; | ||
|
||
// Include fully yanked (all versions were yanked) crates | ||
let username = &user.gh_login; | ||
let json = admin.admin_list(username).await.good(); | ||
|
||
assert_eq!(json.user_email.unwrap(), "foo@example.com"); | ||
assert!(json.user_email_verified); | ||
assert_eq!(json.crates.len(), 2); | ||
|
||
let json_crate_0 = &json.crates[0]; | ||
assert_eq!(json_crate_0.name, "all_yanked"); | ||
assert!(json_crate_0.description.is_none()); | ||
assert_eq!(json_crate_0.downloads, 0); | ||
assert_eq!(json_crate_0.num_versions, 2); | ||
assert!(json_crate_0.yanked); | ||
assert_eq!(json_crate_0.default_version_num, "2.0.0"); | ||
assert_eq!(json_crate_0.num_rev_deps, 0); | ||
|
||
let json_crate_1 = &json.crates[1]; | ||
assert_eq!(json_crate_1.name, "unyanked"); | ||
assert_eq!(json_crate_1.description.as_ref().unwrap(), "My Fun Crate"); | ||
assert_eq!(json_crate_1.downloads, 536); | ||
assert_eq!(json_crate_1.num_versions, 3); | ||
assert!(!json_crate_1.yanked); | ||
assert_eq!(json_crate_1.default_version_num, "1.0.0"); | ||
assert_eq!(json_crate_1.num_rev_deps, 1); | ||
|
||
Ok(()) | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
mod admin; | ||
pub mod downloads; | ||
mod following; | ||
mod list; | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.