From c082594825cadbbe99462708891f0cbcbbeb2049 Mon Sep 17 00:00:00 2001 From: "Calum H." Date: Thu, 11 Jun 2026 20:27:28 +0100 Subject: [PATCH 01/16] fix: underscored users not searchable (#6362) --- ...3f3c54b41e9f606db1f18accee33afdddf49.json} | 4 ++-- .../labrinth/src/database/models/user_item.rs | 4 ++-- apps/labrinth/tests/user.rs | 21 +++++++++++++++++++ 3 files changed, 25 insertions(+), 4 deletions(-) rename apps/labrinth/.sqlx/{query-8d0ae0da359ebd33801f2796c841b9b3cc1a59f7cdee756ac5ce1c459e69a531.json => query-d0cabd1c74fa04c77a02e99e201e3f3c54b41e9f606db1f18accee33afdddf49.json} (76%) diff --git a/apps/labrinth/.sqlx/query-8d0ae0da359ebd33801f2796c841b9b3cc1a59f7cdee756ac5ce1c459e69a531.json b/apps/labrinth/.sqlx/query-d0cabd1c74fa04c77a02e99e201e3f3c54b41e9f606db1f18accee33afdddf49.json similarity index 76% rename from apps/labrinth/.sqlx/query-8d0ae0da359ebd33801f2796c841b9b3cc1a59f7cdee756ac5ce1c459e69a531.json rename to apps/labrinth/.sqlx/query-d0cabd1c74fa04c77a02e99e201e3f3c54b41e9f606db1f18accee33afdddf49.json index 80120ce39..0fea6b35e 100644 --- a/apps/labrinth/.sqlx/query-8d0ae0da359ebd33801f2796c841b9b3cc1a59f7cdee756ac5ce1c459e69a531.json +++ b/apps/labrinth/.sqlx/query-d0cabd1c74fa04c77a02e99e201e3f3c54b41e9f606db1f18accee33afdddf49.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT id, username, avatar_url\n FROM users\n WHERE LOWER(username) LIKE $1 ESCAPE ''\n ORDER BY LOWER(username) = $2 DESC, LOWER(username), username\n LIMIT 25\n ", + "query": "\n SELECT id, username, avatar_url\n FROM users\n WHERE LOWER(username) LIKE $1 ESCAPE '\\'\n ORDER BY LOWER(username) = $2 DESC, LOWER(username), username\n LIMIT 25\n ", "describe": { "columns": [ { @@ -31,5 +31,5 @@ true ] }, - "hash": "8d0ae0da359ebd33801f2796c841b9b3cc1a59f7cdee756ac5ce1c459e69a531" + "hash": "d0cabd1c74fa04c77a02e99e201e3f3c54b41e9f606db1f18accee33afdddf49" } diff --git a/apps/labrinth/src/database/models/user_item.rs b/apps/labrinth/src/database/models/user_item.rs index d0df6460d..57653b260 100644 --- a/apps/labrinth/src/database/models/user_item.rs +++ b/apps/labrinth/src/database/models/user_item.rs @@ -286,13 +286,13 @@ impl DBUser { let escaped_query = format!("{}%", escape_like(&lowercase_query)); let users = sqlx::query!( - " + r#" SELECT id, username, avatar_url FROM users WHERE LOWER(username) LIKE $1 ESCAPE '\' ORDER BY LOWER(username) = $2 DESC, LOWER(username), username LIMIT 25 - ", + "#, escaped_query, lowercase_query ) diff --git a/apps/labrinth/tests/user.rs b/apps/labrinth/tests/user.rs index 5be41d577..3bfd4ac78 100644 --- a/apps/labrinth/tests/user.rs +++ b/apps/labrinth/tests/user.rs @@ -79,6 +79,16 @@ pub async fn search_users_escapes_wildcards_and_limits_results() { .unwrap(); } + sqlx::query( + " + INSERT INTO users (id, username, email, role) + VALUES (2100, 'prefix_under_score', 'prefix_under_score@modrinth.com', 'developer') + ", + ) + .execute(&*test_env.db.pool) + .await + .unwrap(); + let req = test::TestRequest::get() .uri("/v3/users/search?query=prefix") .to_request(); @@ -104,6 +114,17 @@ pub async fn search_users_escapes_wildcards_and_limits_results() { test::read_body_json(resp).await; assert!(users.is_empty()); + let req = test::TestRequest::get() + .uri("/v3/users/search?query=prefix_") + .to_request(); + let resp = test_env.call(req).await; + assert_status!(&resp, actix_http::StatusCode::OK); + + let users: Vec = + test::read_body_json(resp).await; + assert_eq!(users.len(), 1); + assert_eq!(users[0]["username"], "prefix_under_score"); + let req = test::TestRequest::get() .uri("/v3/users/search?query=%20%20") .to_request(); From 64e17c7c1b17bbb32edaed73b737034fa3d92ce5 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Thu, 11 Jun 2026 21:18:44 +0100 Subject: [PATCH 02/16] Include compatible dependencies field in project search (#6366) * include compatible dependency projects field * fix --- ...9056ac842c402bd0941c45ebd1dc6d627d43.json} | 14 +++- .../src/search/backend/typesense/mod.rs | 7 ++ apps/labrinth/src/search/indexing.rs | 81 ++++++++++++------- apps/labrinth/src/search/mod.rs | 9 +++ apps/labrinth/tests/search.rs | 19 +++++ 5 files changed, 96 insertions(+), 34 deletions(-) rename apps/labrinth/.sqlx/{query-20cb8a3d45911a126aa17451b1e6e84e9238dee422cc2b400cea0048a71e4c27.json => query-3afdd6b9070ea0951682facf207b9056ac842c402bd0941c45ebd1dc6d627d43.json} (53%) diff --git a/apps/labrinth/.sqlx/query-20cb8a3d45911a126aa17451b1e6e84e9238dee422cc2b400cea0048a71e4c27.json b/apps/labrinth/.sqlx/query-3afdd6b9070ea0951682facf207b9056ac842c402bd0941c45ebd1dc6d627d43.json similarity index 53% rename from apps/labrinth/.sqlx/query-20cb8a3d45911a126aa17451b1e6e84e9238dee422cc2b400cea0048a71e4c27.json rename to apps/labrinth/.sqlx/query-3afdd6b9070ea0951682facf207b9056ac842c402bd0941c45ebd1dc6d627d43.json index 5d7941bfe..694308e56 100644 --- a/apps/labrinth/.sqlx/query-20cb8a3d45911a126aa17451b1e6e84e9238dee422cc2b400cea0048a71e4c27.json +++ b/apps/labrinth/.sqlx/query-3afdd6b9070ea0951682facf207b9056ac842c402bd0941c45ebd1dc6d627d43.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT DISTINCT v.mod_id dependent_project_id, d.mod_dependency_id dependency_project_id,\n m.name dependency_name, m.slug dependency_slug, m.icon_url dependency_icon_url\n FROM versions v\n INNER JOIN dependencies d ON d.dependent_id = v.id\n INNER JOIN mods m ON m.id = d.mod_dependency_id\n WHERE v.mod_id = ANY($1)\n AND d.mod_dependency_id IS NOT NULL\n AND m.status = ANY($2)\n ", + "query": "\n SELECT DISTINCT v.mod_id dependent_project_id,\n d.mod_dependency_id dependency_project_id,\n d.dependency_type dependency_type,\n m.name dependency_name,\n m.slug dependency_slug,\n m.icon_url dependency_icon_url\n FROM versions v\n INNER JOIN dependencies d ON d.dependent_id = v.id\n INNER JOIN mods m ON m.id = d.mod_dependency_id\n WHERE v.mod_id = ANY($1)\n AND d.mod_dependency_id IS NOT NULL\n AND m.status = ANY($2)\n ", "describe": { "columns": [ { @@ -15,16 +15,21 @@ }, { "ordinal": 2, - "name": "dependency_name", + "name": "dependency_type", "type_info": "Varchar" }, { "ordinal": 3, - "name": "dependency_slug", + "name": "dependency_name", "type_info": "Varchar" }, { "ordinal": 4, + "name": "dependency_slug", + "type_info": "Varchar" + }, + { + "ordinal": 5, "name": "dependency_icon_url", "type_info": "Varchar" } @@ -39,9 +44,10 @@ false, true, false, + false, true, true ] }, - "hash": "20cb8a3d45911a126aa17451b1e6e84e9238dee422cc2b400cea0048a71e4c27" + "hash": "3afdd6b9070ea0951682facf207b9056ac842c402bd0941c45ebd1dc6d627d43" } diff --git a/apps/labrinth/src/search/backend/typesense/mod.rs b/apps/labrinth/src/search/backend/typesense/mod.rs index 5959d9578..750458714 100644 --- a/apps/labrinth/src/search/backend/typesense/mod.rs +++ b/apps/labrinth/src/search/backend/typesense/mod.rs @@ -485,6 +485,13 @@ impl SearchField { sort: false, optional: true, }, + SearchField::CompatibleDependencyProjectIds => TypesenseFieldSpec { + path: "compatible_dependency_project_ids", + ty: "string[]", + facet: true, + sort: false, + optional: true, + }, } } } diff --git a/apps/labrinth/src/search/indexing.rs b/apps/labrinth/src/search/indexing.rs index 314f841f9..96793510f 100644 --- a/apps/labrinth/src/search/indexing.rs +++ b/apps/labrinth/src/search/indexing.rs @@ -21,7 +21,7 @@ use crate::database::models::{ use crate::database::redis::RedisPool; use crate::models::exp; use crate::models::ids::ProjectId; -use crate::models::projects::from_duplicate_version_fields; +use crate::models::projects::{DependencyType, from_duplicate_version_fields}; use crate::models::v2::projects::LegacyProject; use crate::routes::v2_reroute; use crate::search::{SearchProjectDependency, UploadSearchProject}; @@ -124,10 +124,14 @@ pub async fn index_local( info!("Indexing local dependencies!"); let dependencies: DashMap> = - sqlx::query!( - " - SELECT DISTINCT v.mod_id dependent_project_id, d.mod_dependency_id dependency_project_id, - m.name dependency_name, m.slug dependency_slug, m.icon_url dependency_icon_url + sqlx::query!( + " + SELECT DISTINCT v.mod_id dependent_project_id, + d.mod_dependency_id dependency_project_id, + d.dependency_type dependency_type, + m.name dependency_name, + m.slug dependency_slug, + m.icon_url dependency_icon_url FROM versions v INNER JOIN dependencies d ON d.dependent_id = v.id INNER JOIN mods m ON m.id = d.mod_dependency_id @@ -135,32 +139,35 @@ pub async fn index_local( AND d.mod_dependency_id IS NOT NULL AND m.status = ANY($2) ", - &project_ids, - &searchable_statuses, - ) - .fetch(pool) - .try_fold( - DashMap::new(), - |acc: DashMap>, m| { - if let Some(dependency_project_id) = m.dependency_project_id { - acc.entry(DBProjectId(m.dependent_project_id)) - .or_default() - .push(SearchProjectDependency { - project_id: ProjectId::from(DBProjectId( - dependency_project_id, - )) - .to_string(), - name: m.dependency_name, - slug: m.dependency_slug, - icon_url: m.dependency_icon_url, - }); - } + &project_ids, + &searchable_statuses, + ) + .fetch(pool) + .try_fold( + DashMap::new(), + |acc: DashMap>, m| { + if let Some(dependency_project_id) = m.dependency_project_id { + acc.entry(DBProjectId(m.dependent_project_id)) + .or_default() + .push(SearchProjectDependency { + project_id: ProjectId::from(DBProjectId( + dependency_project_id, + )) + .to_string(), + dependency_type: DependencyType::from_string( + &m.dependency_type, + ), + name: m.dependency_name, + slug: m.dependency_slug, + icon_url: m.dependency_icon_url, + }); + } - async move { Ok(acc) } - }, - ) - .await - .wrap_err("failed to fetch project dependencies")?; + async move { Ok(acc) } + }, + ) + .await + .wrap_err("failed to fetch project dependencies")?; struct PartialGallery { url: String, @@ -398,6 +405,18 @@ pub async fn index_local( .iter() .map(|dependency| dependency.project_id.clone()) .collect::>(); + let compatible_dependency_project_ids = dependencies + .iter() + .filter(|dependency| { + matches!( + dependency.dependency_type, + DependencyType::Required + | DependencyType::Optional + | DependencyType::Embedded + ) + }) + .map(|dependency| dependency.project_id.clone()) + .collect::>(); if let Some(versions) = versions.remove(&project.id) { // Aggregated project loader fields @@ -539,6 +558,8 @@ pub async fn index_local( open_source, color: project.color.map(|x| x as u32), dependency_project_ids: dependency_project_ids.clone(), + compatible_dependency_project_ids: + compatible_dependency_project_ids.clone(), dependencies: dependencies.clone(), loader_fields, project_loader_fields: project_loader_fields.clone(), diff --git a/apps/labrinth/src/search/mod.rs b/apps/labrinth/src/search/mod.rs index 90c0f9e3c..0f46a8111 100644 --- a/apps/labrinth/src/search/mod.rs +++ b/apps/labrinth/src/search/mod.rs @@ -2,6 +2,7 @@ use crate::database::redis::RedisPool; use crate::models::exp; use crate::models::exp::minecraft::JavaServerPing; use crate::models::ids::{ProjectId, VersionId}; +use crate::models::projects::DependencyType; use crate::queue::server_ping; use crate::routes::ApiError; use crate::{database::PgPool, env::ENV}; @@ -196,6 +197,7 @@ pub enum SearchField { MinecraftJavaServerContentSupportedGameVersions, MinecraftJavaServerPingData, DependencyProjectIds, + CompatibleDependencyProjectIds, } #[derive(Debug, Error)] @@ -252,6 +254,8 @@ pub struct UploadSearchProject { #[serde(default)] pub dependency_project_ids: Vec, #[serde(default)] + pub compatible_dependency_project_ids: Vec, + #[serde(default)] pub dependencies: Vec, // Hidden fields to get the Project model out of the search results. @@ -267,6 +271,7 @@ pub struct UploadSearchProject { #[derive(Serialize, Deserialize, Debug, Clone)] pub struct SearchProjectDependency { pub project_id: String, + pub dependency_type: DependencyType, pub name: String, pub slug: Option, pub icon_url: Option, @@ -311,6 +316,8 @@ pub struct ResultSearchProject { #[serde(default)] pub dependency_project_ids: Vec, #[serde(default)] + pub compatible_dependency_project_ids: Vec, + #[serde(default)] pub dependencies: Vec, // Hidden fields to get the Project model out of the search results. @@ -350,6 +357,8 @@ impl From for ResultSearchProject { featured_gallery: source.featured_gallery, color: source.color, dependency_project_ids: source.dependency_project_ids, + compatible_dependency_project_ids: source + .compatible_dependency_project_ids, dependencies: source.dependencies, loaders: source.loaders, project_loader_fields: source.project_loader_fields, diff --git a/apps/labrinth/tests/search.rs b/apps/labrinth/tests/search.rs index 9bcba177c..c009aa929 100644 --- a/apps/labrinth/tests/search.rs +++ b/apps/labrinth/tests/search.rs @@ -9,6 +9,7 @@ use common::environment::TestEnvironment; use common::environment::with_test_environment; use common::search::setup_search_projects; use futures::stream::StreamExt; +use labrinth::models::projects::DependencyType; use serde_json::json; use crate::common::api_common::Api; @@ -95,6 +96,12 @@ async fn search_projects() { )]]), vec![7], ), + ( + json!([[format!( + "compatible_dependency_project_ids:{dependency_project_id}" + )]]), + vec![7], + ), ]; // TODO: versions, game versions // Untested: @@ -151,11 +158,23 @@ async fn search_projects() { projects.hits[0].dependency_project_ids[0], dependency_project_id ); + assert_eq!( + projects.hits[0].compatible_dependency_project_ids.len(), + 1 + ); + assert_eq!( + projects.hits[0].compatible_dependency_project_ids[0], + dependency_project_id + ); assert_eq!(projects.hits[0].dependencies.len(), 1); assert_eq!( projects.hits[0].dependencies[0].project_id, dependency_project_id ); + assert_eq!( + projects.hits[0].dependencies[0].dependency_type, + DependencyType::Required + ); assert!( projects.hits[0].dependencies[0] .slug From bfbe66f73b2d3d056140c016558794c71d807dd9 Mon Sep 17 00:00:00 2001 From: Prospector <6166773+Prospector@users.noreply.github.com> Date: Thu, 11 Jun 2026 15:30:39 -0700 Subject: [PATCH 03/16] feat: add product metadata to admin billing (#6371) * feat: add product metadata to admin billing * prepr --- .../ui/admin/AdminBillingChargeCard.vue | 189 ++++++++++++++++++ .../billing/{[charge].vue => [user].vue} | 153 ++------------ 2 files changed, 202 insertions(+), 140 deletions(-) create mode 100644 apps/frontend/src/components/ui/admin/AdminBillingChargeCard.vue rename apps/frontend/src/pages/admin/billing/{[charge].vue => [user].vue} (71%) diff --git a/apps/frontend/src/components/ui/admin/AdminBillingChargeCard.vue b/apps/frontend/src/components/ui/admin/AdminBillingChargeCard.vue new file mode 100644 index 000000000..46f9e7927 --- /dev/null +++ b/apps/frontend/src/components/ui/admin/AdminBillingChargeCard.vue @@ -0,0 +1,189 @@ + + diff --git a/apps/frontend/src/pages/admin/billing/[charge].vue b/apps/frontend/src/pages/admin/billing/[user].vue similarity index 71% rename from apps/frontend/src/pages/admin/billing/[charge].vue rename to apps/frontend/src/pages/admin/billing/[user].vue index 220da9129..936ac39ca 100644 --- a/apps/frontend/src/pages/admin/billing/[charge].vue +++ b/apps/frontend/src/pages/admin/billing/[user].vue @@ -198,115 +198,17 @@
-
-
-
-
- - - - - - - - - - - ⋅ - - - - - - - - - - - - Ended: - - Ends: - Issued: - Due: - {{ formatDateTime(charge.due) }} - ({{ formatRelativeTime(charge.due) }}) - - - Last attempt: - Charged: - {{ formatDateTime(charge.last_attempt) }} - ({{ formatRelativeTime(charge.last_attempt) }}) - - -
- {{ charge.status }} - ⋅ - {{ charge.type }} - ⋅ - {{ formatPrice(charge.amount, charge.currency_code) }} - ⋅ - {{ formatDateTimeShort(charge.due) }} - -
-
-
- -
Charge refunded
-
- - - - - - -
-
-
+ :charge="charge" + :subscription="subscription" + :all-charges="charges" + :charge-index="index" + :charge-count="subscription.charges.length" + @refund="showRefundModal" + @modify="showModifyModal" + />
@@ -334,7 +236,6 @@ import { StyledInput, Toggle, useFormatDateTime, - useFormatPrice, useRelativeTime, useVIntl, } from '@modrinth/ui' @@ -344,21 +245,14 @@ import { useQuery } from '@tanstack/vue-query' import dayjs from 'dayjs' import ModrinthServersIcon from '~/components/brand/ModrinthServersIcon.vue' +import AdminBillingChargeCard from '~/components/ui/admin/AdminBillingChargeCard.vue' const { addNotification } = injectNotificationManager() const { labrinth } = injectModrinthClient() -const formatPrice = useFormatPrice() const formatDateTime = useFormatDateTime({ timeStyle: 'short', dateStyle: 'long', }) -const formatDateTimeShort = useFormatDateTime({ - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: 'numeric', - minute: 'numeric', -}) const vintl = useVIntl() @@ -372,15 +266,15 @@ const messages = defineMessages({ }, }) -const chargeId = useRouteId('charge') +const userId = useRouteId('user') const { data: user, error: userError, suspense: userSuspense, } = useQuery({ - queryKey: ['user', chargeId], - queryFn: () => labrinth.users_v2.get(chargeId), + queryKey: ['user', userId], + queryFn: () => labrinth.users_v2.get(userId), }) onServerPrefetch(userSuspense) @@ -533,27 +427,6 @@ async function modifyCharge() { } modifying.value = false } - -const chargeStatuses = { - open: { - color: 'bg-blue', - }, - processing: { - color: 'bg-orange', - }, - succeeded: { - color: 'bg-green', - }, - failed: { - color: 'bg-red', - }, - cancelled: { - color: 'bg-red', - }, - expiring: { - color: 'bg-orange', - }, -}