mirror of
https://github.com/modrinth/code.git
synced 2026-09-04 22:10:15 +00:00
fix: moderation issues with shared instances
This commit is contained in:
@@ -64,11 +64,18 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="report.item_type === 'shared-instance'" class="item-info">
|
<div v-else-if="report.item_type === 'shared-instance'" class="item-info">
|
||||||
<div class="backed-svg" :class="{ raised: raised }">
|
<Avatar
|
||||||
|
v-if="report.shared_instance"
|
||||||
|
:src="report.shared_instance.icon"
|
||||||
|
size="xs"
|
||||||
|
no-shadow
|
||||||
|
:raised="raised"
|
||||||
|
/>
|
||||||
|
<div v-else class="backed-svg" :class="{ raised: raised }">
|
||||||
<BoxesIcon />
|
<BoxesIcon />
|
||||||
</div>
|
</div>
|
||||||
<div class="stacked">
|
<div class="stacked">
|
||||||
<span class="title">Shared instance</span>
|
<span class="title">{{ report.shared_instance?.name ?? 'Shared instance' }}</span>
|
||||||
<span>
|
<span>
|
||||||
Version {{ report.shared_instance_version_id ?? 'unknown' }} ·
|
Version {{ report.shared_instance_version_id ?? 'unknown' }} ·
|
||||||
<CopyCode :text="report.item_id" />
|
<CopyCode :text="report.item_id" />
|
||||||
@@ -89,8 +96,7 @@
|
|||||||
<ThreadSummary
|
<ThreadSummary
|
||||||
v-if="thread"
|
v-if="thread"
|
||||||
:thread="thread"
|
:thread="thread"
|
||||||
class="thread-summary"
|
class="thread-summary !bg-surface-2"
|
||||||
:raised="raised"
|
|
||||||
:link="`/${moderation ? 'moderation' : 'dashboard'}/report/${report.id}`"
|
:link="`/${moderation ? 'moderation' : 'dashboard'}/report/${report.id}`"
|
||||||
:auth="auth"
|
:auth="auth"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="pb-20">
|
||||||
<section>
|
<section>
|
||||||
<Breadcrumbs
|
<Breadcrumbs
|
||||||
v-if="breadcrumbsStack"
|
v-if="breadcrumbsStack"
|
||||||
@@ -111,6 +111,16 @@ const { data: project } = useQuery({
|
|||||||
enabled: computed(() => !!projectId.value),
|
enabled: computed(() => !!projectId.value),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const sharedInstanceId = computed(() =>
|
||||||
|
rawReport.value?.item_type === 'shared-instance' ? rawReport.value.item_id : null,
|
||||||
|
)
|
||||||
|
|
||||||
|
const { data: sharedInstance } = useQuery({
|
||||||
|
queryKey: computed(() => ['shared-instance', sharedInstanceId.value]),
|
||||||
|
queryFn: () => client.sharedinstances.instances_v1.get(sharedInstanceId.value),
|
||||||
|
enabled: computed(() => !!sharedInstanceId.value),
|
||||||
|
})
|
||||||
|
|
||||||
// Assemble the full report object
|
// Assemble the full report object
|
||||||
const report = computed(() => {
|
const report = computed(() => {
|
||||||
if (!rawReport.value) return null
|
if (!rawReport.value) return null
|
||||||
@@ -118,6 +128,7 @@ const report = computed(() => {
|
|||||||
...rawReport.value,
|
...rawReport.value,
|
||||||
project: project.value ?? null,
|
project: project.value ?? null,
|
||||||
version: version.value ?? null,
|
version: version.value ?? null,
|
||||||
|
shared_instance: sharedInstance.value ?? null,
|
||||||
reporterUser: (users.value || []).find((user) => user.id === rawReport.value.reporter),
|
reporterUser: (users.value || []).find((user) => user.id === rawReport.value.reporter),
|
||||||
user:
|
user:
|
||||||
rawReport.value.item_type === 'user'
|
rawReport.value.item_type === 'user'
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
:moderation="moderation"
|
:moderation="moderation"
|
||||||
raised
|
raised
|
||||||
:auth="auth"
|
:auth="auth"
|
||||||
class="universal-card recessed"
|
class="card-shadow mb-4 rounded-2xl border border-solid border-surface-4 bg-surface-3 p-4"
|
||||||
/>
|
/>
|
||||||
<p v-if="filteredReports.length === 0">You don't have any active reports.</p>
|
<p v-if="filteredReports.length === 0">You don't have any active reports.</p>
|
||||||
</template>
|
</template>
|
||||||
@@ -51,7 +51,7 @@ const MAX_REPORTS = 1500
|
|||||||
|
|
||||||
const { data: rawReportsData } = useQuery({
|
const { data: rawReportsData } = useQuery({
|
||||||
queryKey: ['reports', MAX_REPORTS],
|
queryKey: ['reports', MAX_REPORTS],
|
||||||
queryFn: () => client.labrinth.reports_v3.list({ count: MAX_REPORTS }),
|
queryFn: () => client.labrinth.reports_v3.list({ count: MAX_REPORTS, all: false }),
|
||||||
placeholderData: [],
|
placeholderData: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -67,6 +67,13 @@ const versionReports = computed(() =>
|
|||||||
const versionIds = computed(() => [
|
const versionIds = computed(() => [
|
||||||
...new Set(versionReports.value.map((report) => report.item_id)),
|
...new Set(versionReports.value.map((report) => report.item_id)),
|
||||||
])
|
])
|
||||||
|
const sharedInstanceIds = computed(() => [
|
||||||
|
...new Set(
|
||||||
|
rawReports.value
|
||||||
|
.filter((report) => report.item_type === 'shared-instance')
|
||||||
|
.map((report) => report.item_id),
|
||||||
|
),
|
||||||
|
])
|
||||||
const userIds = computed(() => [...new Set(reporterUsers.value.concat(reportedUsers.value))])
|
const userIds = computed(() => [...new Set(reporterUsers.value.concat(reportedUsers.value))])
|
||||||
const threadIds = computed(() => [
|
const threadIds = computed(() => [
|
||||||
...new Set(
|
...new Set(
|
||||||
@@ -94,6 +101,21 @@ const { data: versions } = useQuery({
|
|||||||
placeholderData: [],
|
placeholderData: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { data: sharedInstances } = useQuery({
|
||||||
|
queryKey: computed(() => ['shared-instances', sharedInstanceIds.value]),
|
||||||
|
queryFn: async () => {
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
sharedInstanceIds.value.map(async (id) => ({
|
||||||
|
id,
|
||||||
|
instance: await client.sharedinstances.instances_v1.get(id),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
return results.flatMap((result) => (result.status === 'fulfilled' ? [result.value] : []))
|
||||||
|
},
|
||||||
|
enabled: computed(() => sharedInstanceIds.value.length > 0),
|
||||||
|
placeholderData: [],
|
||||||
|
})
|
||||||
|
|
||||||
const { data: threads } = useQuery({
|
const { data: threads } = useQuery({
|
||||||
queryKey: computed(() => ['threads', threadIds.value]),
|
queryKey: computed(() => ['threads', threadIds.value]),
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
@@ -123,6 +145,9 @@ const { data: projects } = useQuery({
|
|||||||
const userMap = computed(() => new Map(users.value.map((u) => [u.id, u])))
|
const userMap = computed(() => new Map(users.value.map((u) => [u.id, u])))
|
||||||
const versionMap = computed(() => new Map(versions.value.map((v) => [v.id, v])))
|
const versionMap = computed(() => new Map(versions.value.map((v) => [v.id, v])))
|
||||||
const projectMap = computed(() => new Map(projects.value.map((p) => [p.id, p])))
|
const projectMap = computed(() => new Map(projects.value.map((p) => [p.id, p])))
|
||||||
|
const sharedInstanceMap = computed(
|
||||||
|
() => new Map(sharedInstances.value.map(({ id, instance }) => [id, instance])),
|
||||||
|
)
|
||||||
const threadMap = computed(() => new Map(threads.value.map((t) => [t.id, t])))
|
const threadMap = computed(() => new Map(threads.value.map((t) => [t.id, t])))
|
||||||
|
|
||||||
const reports = computed(() =>
|
const reports = computed(() =>
|
||||||
@@ -136,6 +161,8 @@ const reports = computed(() =>
|
|||||||
} else if (report.item_type === 'version') {
|
} else if (report.item_type === 'version') {
|
||||||
enrichedReport.version = versionMap.value.get(report.item_id)
|
enrichedReport.version = versionMap.value.get(report.item_id)
|
||||||
enrichedReport.project = projectMap.value.get(enrichedReport.version?.project_id)
|
enrichedReport.project = projectMap.value.get(enrichedReport.version?.project_id)
|
||||||
|
} else if (report.item_type === 'shared-instance') {
|
||||||
|
enrichedReport.shared_instance = sharedInstanceMap.value.get(report.item_id)
|
||||||
}
|
}
|
||||||
if (report.thread_id) {
|
if (report.thread_id) {
|
||||||
const thread = threadMap.value.get(report.thread_id)
|
const thread = threadMap.value.get(report.thread_id)
|
||||||
@@ -149,7 +176,7 @@ const reports = computed(() =>
|
|||||||
const filteredReports = computed(() =>
|
const filteredReports = computed(() =>
|
||||||
reports.value?.filter(
|
reports.value?.filter(
|
||||||
(x) =>
|
(x) =>
|
||||||
(props.moderation || x.reporterUser?.id === props.auth.user.id) &&
|
(props.moderation || x.reporter === props.auth.user.id) &&
|
||||||
(viewMode.value === 'open' ? x.open : !x.open) &&
|
(viewMode.value === 'open' ? x.open : !x.open) &&
|
||||||
(reasonFilter.value === 'All' || reasonFilter.value === x.report_type),
|
(reasonFilter.value === 'All' || reasonFilter.value === x.report_type),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="flex flex-col gap-4 pb-20 lg:pl-4 lg:pt-1.5">
|
||||||
<section class="universal-card">
|
<h2 class="m-0 text-xl font-semibold text-contrast md:text-2xl">
|
||||||
<h2 class="text-2xl">{{ formatMessage(messages.reportsTitle) }}</h2>
|
{{ formatMessage(messages.reportsTitle) }}
|
||||||
<ReportsList :auth="auth" />
|
</h2>
|
||||||
</section>
|
<ReportsList :auth="auth" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|||||||
@@ -235,9 +235,12 @@ const { data: allReports } = await useLazyAsyncData('new-moderation-reports', as
|
|||||||
let reports: Labrinth.Reports.v3.Report[]
|
let reports: Labrinth.Reports.v3.Report[]
|
||||||
let hasMoreReports = true
|
let hasMoreReports = true
|
||||||
while (hasMoreReports) {
|
while (hasMoreReports) {
|
||||||
reports = (await useBaseFetch(`report?count=${REPORT_ENDPOINT_COUNT}&offset=${currentOffset}`, {
|
reports = (await useBaseFetch(
|
||||||
apiVersion: 3,
|
`report?count=${REPORT_ENDPOINT_COUNT}&offset=${currentOffset}&all=true`,
|
||||||
})) as Labrinth.Reports.v3.Report[]
|
{
|
||||||
|
apiVersion: 3,
|
||||||
|
},
|
||||||
|
)) as Labrinth.Reports.v3.Report[]
|
||||||
|
|
||||||
hasMoreReports = reports.length > 0
|
hasMoreReports = reports.length > 0
|
||||||
if (!hasMoreReports) {
|
if (!hasMoreReports) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use crate::database::models::ids::*;
|
|||||||
use crate::database::models::notifications_template_item::{
|
use crate::database::models::notifications_template_item::{
|
||||||
NotificationTemplate, get_or_set_cached_dynamic_html,
|
NotificationTemplate, get_or_set_cached_dynamic_html,
|
||||||
};
|
};
|
||||||
|
use crate::database::models::report_item::DBReport;
|
||||||
use crate::database::models::{
|
use crate::database::models::{
|
||||||
DBOrganization, DBProject, DBUser, DatabaseError,
|
DBOrganization, DBProject, DBUser, DatabaseError,
|
||||||
};
|
};
|
||||||
@@ -11,10 +12,12 @@ use crate::env::ENV;
|
|||||||
use crate::models::v3::notifications::NotificationBody;
|
use crate::models::v3::notifications::NotificationBody;
|
||||||
use crate::routes::ApiError;
|
use crate::routes::ApiError;
|
||||||
use crate::util::error::Context;
|
use crate::util::error::Context;
|
||||||
|
use crate::util::http::HTTP_CLIENT;
|
||||||
use ariadne::ids::base62_impl::to_base62;
|
use ariadne::ids::base62_impl::to_base62;
|
||||||
use futures::TryFutureExt;
|
use futures::TryFutureExt;
|
||||||
use lettre::Message;
|
use lettre::Message;
|
||||||
use lettre::message::{Mailbox, MultiPart, SinglePart};
|
use lettre::message::{Mailbox, MultiPart, SinglePart};
|
||||||
|
use serde::Deserialize;
|
||||||
use sqlx::query;
|
use sqlx::query;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -312,6 +315,66 @@ enum EmailTemplate {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct SharedInstance {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn resolve_report_title(
|
||||||
|
exec: &mut PgTransaction<'_>,
|
||||||
|
report_id: DBReportId,
|
||||||
|
title: String,
|
||||||
|
) -> Result<String, ApiError> {
|
||||||
|
if title != "unknown" {
|
||||||
|
return Ok(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(shared_instance_id) = DBReport::get(report_id, &mut *exec)
|
||||||
|
.await?
|
||||||
|
.and_then(|report| report.shared_instance_id)
|
||||||
|
else {
|
||||||
|
return Ok(title);
|
||||||
|
};
|
||||||
|
|
||||||
|
let instance_id = to_base62(shared_instance_id.0 as u64);
|
||||||
|
let fallback = format!("shared instance {instance_id}");
|
||||||
|
let response = HTTP_CLIENT
|
||||||
|
.get(format!(
|
||||||
|
"{}/v1/instances/{instance_id}",
|
||||||
|
ENV.SHARED_INSTANCES_URL
|
||||||
|
))
|
||||||
|
.bearer_auth(&ENV.SHARED_INSTANCES_KEY)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.and_then(reqwest::Response::error_for_status);
|
||||||
|
|
||||||
|
let response = match response {
|
||||||
|
Ok(response) => response,
|
||||||
|
Err(error) => {
|
||||||
|
warn!(
|
||||||
|
%error,
|
||||||
|
report_id = report_id.0,
|
||||||
|
%instance_id,
|
||||||
|
"Failed to fetch shared instance name for report email"
|
||||||
|
);
|
||||||
|
return Ok(fallback);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match response.json::<SharedInstance>().await {
|
||||||
|
Ok(instance) => Ok(instance.name),
|
||||||
|
Err(error) => {
|
||||||
|
warn!(
|
||||||
|
%error,
|
||||||
|
report_id = report_id.0,
|
||||||
|
%instance_id,
|
||||||
|
"Failed to parse shared instance name for report email"
|
||||||
|
);
|
||||||
|
Ok(fallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn collect_template_variables(
|
async fn collect_template_variables(
|
||||||
exec: &mut PgTransaction<'_>,
|
exec: &mut PgTransaction<'_>,
|
||||||
redis: &RedisPool,
|
redis: &RedisPool,
|
||||||
@@ -359,7 +422,15 @@ async fn collect_template_variables(
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
map.insert(REPORT_ID, to_base62(report_id.0));
|
map.insert(REPORT_ID, to_base62(report_id.0));
|
||||||
map.insert(REPORT_TITLE, result.title);
|
map.insert(
|
||||||
|
REPORT_TITLE,
|
||||||
|
resolve_report_title(
|
||||||
|
exec,
|
||||||
|
DBReportId(report_id.0 as i64),
|
||||||
|
result.title,
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
);
|
||||||
map.insert(REPORT_DATE, date_human_readable(result.created));
|
map.insert(REPORT_DATE, date_human_readable(result.created));
|
||||||
Ok(EmailTemplate::Static(map))
|
Ok(EmailTemplate::Static(map))
|
||||||
}
|
}
|
||||||
@@ -380,7 +451,15 @@ async fn collect_template_variables(
|
|||||||
.fetch_one(&mut *exec)
|
.fetch_one(&mut *exec)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
map.insert(REPORT_TITLE, result.title);
|
map.insert(
|
||||||
|
REPORT_TITLE,
|
||||||
|
resolve_report_title(
|
||||||
|
exec,
|
||||||
|
DBReportId(report_id.0 as i64),
|
||||||
|
result.title,
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
);
|
||||||
map.insert(NEWREPORT_ID, to_base62(report_id.0));
|
map.insert(NEWREPORT_ID, to_base62(report_id.0));
|
||||||
Ok(EmailTemplate::Static(map))
|
Ok(EmailTemplate::Static(map))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,16 +60,13 @@ pub async fn report_create(
|
|||||||
pub struct ReportsRequestOptions {
|
pub struct ReportsRequestOptions {
|
||||||
#[serde(default = "default_count")]
|
#[serde(default = "default_count")]
|
||||||
count: u16,
|
count: u16,
|
||||||
#[serde(default = "default_all")]
|
#[serde(default)]
|
||||||
all: bool,
|
all: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_count() -> u16 {
|
fn default_count() -> u16 {
|
||||||
100
|
100
|
||||||
}
|
}
|
||||||
fn default_all() -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get open reports for the current user.
|
/// Get open reports for the current user.
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
|
|||||||
@@ -320,16 +320,13 @@ pub struct ReportsRequestOptions {
|
|||||||
pub count: u16,
|
pub count: u16,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub offset: u32,
|
pub offset: u32,
|
||||||
#[serde(default = "default_all")]
|
#[serde(default)]
|
||||||
pub all: bool,
|
pub all: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_count() -> u16 {
|
fn default_count() -> u16 {
|
||||||
100
|
100
|
||||||
}
|
}
|
||||||
fn default_all() -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
tag = "reports",
|
tag = "reports",
|
||||||
|
|||||||
@@ -44,18 +44,17 @@ pub use self::shared::{
|
|||||||
SharedInstanceExternalFilePreview, SharedInstanceInstallPreview,
|
SharedInstanceExternalFilePreview, SharedInstanceInstallPreview,
|
||||||
SharedInstanceInvite, SharedInstanceInviteInstallPreview,
|
SharedInstanceInvite, SharedInstanceInviteInstallPreview,
|
||||||
SharedInstanceInviteLink, SharedInstanceJoinType,
|
SharedInstanceInviteLink, SharedInstanceJoinType,
|
||||||
SharedInstancePublishPreview,
|
SharedInstancePublishPreview, SharedInstanceUpdateDiff,
|
||||||
SharedInstanceUpdateDiff, SharedInstanceUpdateDiffType,
|
SharedInstanceUpdateDiffType, SharedInstanceUpdatePreview,
|
||||||
SharedInstanceUpdatePreview, SharedInstanceUser, SharedInstanceUsers,
|
SharedInstanceUser, SharedInstanceUsers,
|
||||||
accept_pending_shared_instance_invite,
|
accept_pending_shared_instance_invite,
|
||||||
accept_shared_instance_invite_for_install,
|
accept_shared_instance_invite_for_install,
|
||||||
can_active_user_use_shared_instances, create_shared_instance_invite_link,
|
can_active_user_use_shared_instances, create_shared_instance_invite_link,
|
||||||
decline_pending_shared_instance_invite,
|
decline_pending_shared_instance_invite,
|
||||||
get_shared_instance_install_preview, get_shared_instance_publish_preview,
|
get_shared_instance_install_preview, get_shared_instance_invites,
|
||||||
get_shared_instance_invites, get_shared_instance_update_preview,
|
get_shared_instance_publish_preview, get_shared_instance_update_preview,
|
||||||
get_shared_instance_users, install_shared_instance,
|
get_shared_instance_users, install_shared_instance,
|
||||||
invite_shared_instance_users, publish_shared_instance,
|
invite_shared_instance_users, publish_shared_instance,
|
||||||
remove_shared_instance_users, revoke_shared_instance_invite,
|
remove_shared_instance_users, revoke_shared_instance_invite,
|
||||||
unlink_shared_instance, unpublish_shared_instance,
|
unlink_shared_instance, unpublish_shared_instance, update_shared_instance,
|
||||||
update_shared_instance,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -122,9 +122,9 @@ pub use self::types::{
|
|||||||
SharedInstanceExternalFilePreview, SharedInstanceInstallPreview,
|
SharedInstanceExternalFilePreview, SharedInstanceInstallPreview,
|
||||||
SharedInstanceInvite, SharedInstanceInviteInstallPreview,
|
SharedInstanceInvite, SharedInstanceInviteInstallPreview,
|
||||||
SharedInstanceInviteLink, SharedInstanceJoinType,
|
SharedInstanceInviteLink, SharedInstanceJoinType,
|
||||||
SharedInstancePublishPreview,
|
SharedInstancePublishPreview, SharedInstanceUpdateDiff,
|
||||||
SharedInstanceUpdateDiff, SharedInstanceUpdateDiffType,
|
SharedInstanceUpdateDiffType, SharedInstanceUpdatePreview,
|
||||||
SharedInstanceUpdatePreview, SharedInstanceUser, SharedInstanceUsers,
|
SharedInstanceUser, SharedInstanceUsers,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn can_active_user_use_shared_instances() -> crate::Result<bool> {
|
pub async fn can_active_user_use_shared_instances() -> crate::Result<bool> {
|
||||||
|
|||||||
Reference in New Issue
Block a user